packing-bingo 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jin-hiroo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,401 @@
1
- # Temporary Holding Version
1
+ # Packing Bingo (持ちものビンゴ)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **A React component that turns a school-trip packing checklist into a 4×4 bingo card: students stamp each item as it goes in the bag, complete lines, and get combos, fanfares and fireworks. One component, no runtime dependencies, safe to render on the server.**
4
+
5
+ [![npm version](https://img.shields.io/npm/v/packing-bingo?logo=npm)](https://www.npmjs.com/package/packing-bingo)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
7
+ [![React 18 or later](https://img.shields.io/badge/React-%E2%89%A518-61DAFB?logo=react&logoColor=black)](#at-a-glance)
8
+ [![TypeScript types included](https://img.shields.io/badge/types-TypeScript-3178C6?logo=typescript&logoColor=white)](#api)
9
+ [![Standalone single HTML file](https://img.shields.io/badge/standalone-single%20HTML%20file-E34F26?logo=html5&logoColor=white)](#standalone-page)
10
+
11
+ [Quick start](#quick-start) · [Recipes](#recipes) · [API](#api) · [Integration notes](#integration-notes) · [Without React](#without-react) · [日本語](#日本語) · [Changelog](CHANGELOG.md)
12
+
13
+ <p align="center">
14
+ <a href="docs/images/bingo-desktop.png"><img src="docs/images/bingo-desktop.png" alt="A 4×4 bingo card of packing items. A diagonal line has just been completed: a large BINGO! banner, a +100 score popup and confetti are on screen, and the score panel shows 8/16 items, 2 lines and a 2-tap combo." width="560"></a>
15
+ </p>
16
+
17
+ _The second line completes on a two-tap combo: the line is drawn, the BINGO! banner and +100 pop up, and confetti bursts. The default UI is Japanese: 持ちものビンゴ = "Packing Bingo", そろった = "packed", 2ライン目 = "2nd line". An English text set is included._
18
+
19
+ <p>
20
+ <a href="docs/images/phone-reach.png"><img src="docs/images/phone-reach.png" alt="Phone layout with one line completed and a dashed, pulsing square labeled リーチ (reach) on the last missing item." height="320"></a>
21
+ <a href="docs/images/phone-combo.png"><img src="docs/images/phone-combo.png" alt="Phone layout during a three-tap combo: +20 and +30 popups, 2 COMBO! and 3 COMBO! labels, a draining combo gauge and two リーチ callouts." height="320"></a>
22
+ <a href="docs/images/phone-perfect.png"><img src="docs/images/phone-perfect.png" alt="Phone layout after all 16 items are stamped: a 準備完了 (ready) stamp, the Perfect! card with score 3,410, max combo ×10 and 10 lines, with confetti and falling item emoji." height="320"></a>
23
+ </p>
24
+
25
+ _On a phone, left to right: one line done and a リーチ ("reach") badge on the square that would finish the next one; a three-tap combo; the Perfect! screen with the final score, fireworks and falling item emoji._
26
+
27
+ ## What it does
28
+
29
+ Packing the night before a school trip is a chore. Packing Bingo puts the 16 items on a bingo card, so every packed item is a stamp, and every few items there's something to celebrate.
30
+
31
+ - **Every tap gets feedback.** A stamp, a ripple, sparks and a rising note. Taps less than 2.5 s apart chain into a combo.
32
+ - **There's always a next goal.** A square that would complete a line pulses with a リーチ ("reach") badge, and the status line names the item.
33
+ - **Lines and the full card are celebrated.** Any row, column or diagonal (10 lines in all) is a bingo, with a banner and confetti. Stamping all 16 plays a finale with fireworks and a score card.
34
+ - **Points reward finishing,** not tapping on and off. See [Scoring](#scoring).
35
+ - **It's considerate by default.** Squares are real checkboxes, so the keyboard works. `prefers-reduced-motion` is respected. The sound is synthesized (no audio files) and can be muted.
36
+
37
+ ## At a glance
38
+
39
+ | Area | Details |
40
+ | --- | --- |
41
+ | Install | `npm install packing-bingo`: ESM and CommonJS, TypeScript types included |
42
+ | Requires | React 18 or later and `react-dom`. No other dependencies, no CSS import, no provider |
43
+ | Size | About 13 kB minified and gzipped, CSS included (React not counted) |
44
+ | Server rendering | Ships with `"use client"`. On the server it renders an empty `<div>`, and the card is built after mount |
45
+ | Styling | Injects one `<style>` into `<head>`. Every class is prefixed `pkb-`. Themable with CSS variables, and it sizes itself to its container |
46
+ | Network | Loads two fonts from Google Fonts, unless you pass `fonts={false}` |
47
+ | Storage | localStorage, only for `storageKey` progress and the mute setting |
48
+ | Browsers | Chrome and Edge 105+, Safari 16+, Firefox 121+. Only tested in Chromium so far ([details](#browser-support-and-testing)) |
49
+
50
+ ## Quick start
51
+
52
+ ```sh
53
+ npm install packing-bingo # or: pnpm add packing-bingo / yarn add packing-bingo
54
+ ```
55
+
56
+ ```tsx
57
+ import { PackingBingo } from "packing-bingo";
58
+
59
+ export default function PackingPage() {
60
+ return <PackingBingo storageKey="kyoto-trip-2026" />;
61
+ }
62
+ ```
63
+
64
+ That's the whole setup. The card fills the width of its container, up to 720 px. With `storageKey`, each student's checks are saved in their browser, so they can close the page and come back.
65
+
66
+ **Next.js App Router:** you can use the component straight from a Server Component page, as above, including `items` and `messages={en}`. Functions you write can't cross from a Server Component to a Client Component, though, so callbacks like `onChange` or `onBingo` stop the build with "Event handlers cannot be passed to Client Component props". For those, wrap the card in your own client component:
67
+
68
+ ```tsx
69
+ // app/packing/PackingCard.tsx
70
+ "use client";
71
+
72
+ import { PackingBingo } from "packing-bingo";
73
+
74
+ export function PackingCard() {
75
+ return <PackingBingo storageKey="kyoto-trip-2026" onBingo={(e) => console.log(e)} />;
76
+ }
77
+ ```
78
+
79
+ ## Recipes
80
+
81
+ ### Your own items and text
82
+
83
+ ```tsx
84
+ import { PackingBingo, en, type BingoItem } from "packing-bingo";
85
+
86
+ const items: BingoItem[] = [
87
+ { emoji: "🛂", label: "Passport" },
88
+ { emoji: "🎫", label: "Tickets" },
89
+ // … exactly 16 items, row by row from the top left
90
+ ];
91
+
92
+ <PackingBingo items={items} messages={en} title="Kyoto Trip" tag="Class 2-B" storageKey="kyoto-2b" />;
93
+ ```
94
+
95
+ - `items` needs exactly 16 entries. Any other count throws `[packing-bingo] "items" must have exactly 16 entries`.
96
+ - `messages` is merged over the Japanese defaults, so `messages={{ reset: "Start over" }}` changes just that label. `en` is a complete English set. See [Messages](#messages) for every key.
97
+ - On narrow cards, labels wrap after a `・`, so `常備薬・酔い止め` breaks as 常備薬・ / 酔い止め.
98
+
99
+ ### Save progress on your server
100
+
101
+ `storageKey` saves progress in one browser. To make progress follow a student to another device, keep the checks in your own state and pass them as `checked`, which makes the card controlled:
102
+
103
+ ```tsx
104
+ import { useEffect, useRef, useState } from "react";
105
+ import { PackingBingo } from "packing-bingo";
106
+
107
+ export function PackingCard({ studentId }: { studentId: string }) {
108
+ const [checked, setChecked] = useState<boolean[] | null>(null);
109
+ const saving = useRef(Promise.resolve());
110
+
111
+ useEffect(() => {
112
+ let current = true; // ignore a response that arrives after the student has changed
113
+ setChecked(null);
114
+ fetch(`/api/packing/${studentId}`)
115
+ .then((res) => res.json())
116
+ .then((data) => {
117
+ if (current) setChecked(data.checked ?? Array(16).fill(false));
118
+ });
119
+ return () => {
120
+ current = false;
121
+ };
122
+ }, [studentId]);
123
+
124
+ if (!checked) return null; // or a placeholder
125
+
126
+ const save = (next: boolean[]) => {
127
+ setChecked(next);
128
+ // Send one request at a time, so a slow earlier save can't overwrite a later one.
129
+ saving.current = saving.current
130
+ .then(() => fetch(`/api/packing/${studentId}`, { method: "PUT", body: JSON.stringify({ checked: next }) }))
131
+ .then(() => undefined, () => undefined);
132
+ };
133
+
134
+ return <PackingBingo checked={checked} onChange={save} />;
135
+ }
136
+ ```
137
+
138
+ Students tap faster than requests finish, so the example sends the saves one after another, and it drops a load that arrives after `studentId` has changed. Each save sends all 16 checks, so a later save always holds the latest state.
139
+
140
+ How a controlled card behaves:
141
+
142
+ - A tap doesn't change the card by itself. It calls `onChange` with the 16 booleans the user asked for, and the card follows when `checked` changes. If you don't update `checked`, the tap is undone.
143
+ - When `checked` changes by exactly one item, the card plays it like a tap: stamp, sound, and any bingo or finale. A bigger change, such as data arriving from your server, is applied quietly, with no fanfare and no bonus for lines completed at once.
144
+ - The reset button calls `onChange` with 16 `false` values.
145
+ - `storageKey` is ignored.
146
+
147
+ ### React to bingos
148
+
149
+ ```tsx
150
+ <PackingBingo
151
+ storageKey="kyoto-trip-2026"
152
+ onBingo={({ newLines, totalLines, score }) => analytics.track("bingo", { totalLines, score })}
153
+ onPerfect={({ score, maxCombo }) => analytics.track("perfect", { score, maxCombo })}
154
+ />
155
+ ```
156
+
157
+ - `onBingo` fires when a tap completes one or more lines. On the tap that completes the whole card, `onPerfect` fires instead.
158
+ - `onChange` fires only for what the user does: a tap, the reset button, or `ref.reset()`. Changes you make through `checked` or `ref.setChecked()` don't trigger it.
159
+ - `onBingo` and `onPerfect` fire whenever a change plays like a tap, including a one-item change of `checked`. Quiet updates and the first render fire nothing.
160
+ - `onChange` runs first, then `onBingo` or `onPerfect`, after the card has updated. You can reset or unmount the card from inside any of them.
161
+
162
+ ### Control the card from outside
163
+
164
+ ```tsx
165
+ import { useRef } from "react";
166
+ import { PackingBingo, type PackingBingoHandle } from "packing-bingo";
167
+
168
+ function PackingPage() {
169
+ const card = useRef<PackingBingoHandle>(null);
170
+ return (
171
+ <>
172
+ <button onClick={() => card.current?.reset()}>Start over</button>
173
+ <PackingBingo ref={card} storageKey="kyoto-trip-2026" />
174
+ </>
175
+ );
176
+ }
177
+ ```
178
+
179
+ The handle has `reset()`, `setChecked()` and `getState()`. See [Ref handle](#ref-handle). With a controlled card, change `checked` instead of calling `setChecked()`.
180
+
181
+ ### Theme and layout
182
+
183
+ Set any of these CSS variables on the component or an ancestor:
184
+
185
+ | Variable | Used for | Default |
186
+ | --- | --- | --- |
187
+ | `--pkb-sun` | Board background | `#FFD54F` |
188
+ | `--pkb-ink` | Lines, borders and text | `#2A1F14` |
189
+ | `--pkb-paper` | Squares | `#FFFDF6` |
190
+ | `--pkb-red` | Stamps and accents | `#E8473B` |
191
+ | `--pkb-mint` | Completed lines | `#BDEBD2` |
192
+ | `--pkb-font-fallback` | Font used before or instead of the display fonts | `sans-serif` |
193
+
194
+ ```tsx
195
+ <PackingBingo style={{ "--pkb-sun": "#9AD0F5" } as React.CSSProperties} />
196
+ ```
197
+
198
+ The card takes the full width of its container, up to 720 px, and centers itself. When its container is narrower than 560 px, it switches to a compact layout. This uses a container query, so the compact layout also kicks in inside a narrow sidebar on a wide screen. The height follows from the width. Because the server renders an empty `<div>`, the card appears after hydration; if layout shift matters, give its wrapper a `min-height`.
199
+
200
+ ## API
201
+
202
+ ### Props
203
+
204
+ | Prop | Type | Default | What it does |
205
+ | --- | --- | --- | --- |
206
+ | `items` | `readonly BingoItem[]` (exactly 16) | `DEFAULT_ITEMS` (Japanese school-trip list) | The squares, row by row: `{ emoji, label }` |
207
+ | `messages` | `Partial<Messages>` | `ja` | UI text, merged over the Japanese defaults. `en` is included |
208
+ | `title`, `tag`, `subtitle` | `string` | from `messages` | The heading, the small label above it, and the line below it |
209
+ | `storageKey` | `string` | none | Save the checks in localStorage under this key. Uncontrolled cards only |
210
+ | `defaultChecked` | `readonly boolean[]` (16) | all `false` | Initial checks of an uncontrolled card, used when nothing is saved under `storageKey` |
211
+ | `checked` | `readonly boolean[]` (16) | none | Makes the card controlled. See [Save progress on your server](#save-progress-on-your-server) |
212
+ | `onChange` | `(checked: boolean[]) => void` | none | The user tapped or reset. For a controlled card, this is the requested state |
213
+ | `onBingo` | `(event: BingoEvent) => void` | none | A tap completed one or more lines |
214
+ | `onPerfect` | `(event: PerfectEvent) => void` | none | A tap completed the card |
215
+ | `sound` | `boolean` | `true` | Initial sound setting, until the user picks one with the 🔊 button |
216
+ | `fonts` | `boolean` | `true` | Load Dela Gothic One and M PLUS Rounded 1c from Google Fonts |
217
+ | `demo` | `boolean` | `false` | Pre-fill six items (when nothing is saved) and show demo buttons |
218
+ | `className`, `style` | | none | Applied to the wrapper `<div>` |
219
+ | `ref` | `Ref<PackingBingoHandle>` | none | See [Ref handle](#ref-handle) |
220
+
221
+ Props are compared by value, so inline arrays and objects are fine. When the content of `items`, `title`, `tag`, `subtitle`, `storageKey`, `sound`, `fonts`, `demo` or the text in `messages` changes, the card is rebuilt. The checks are kept, unless `storageKey` changed, in which case the card loads that key's own progress. The score is then recomputed, as described in [Scoring](#scoring). Message functions can use your props or state: when `messages` changes, the status line is redrawn without a rebuild.
222
+
223
+ ### Events
224
+
225
+ ```ts
226
+ interface BingoEvent {
227
+ newLines: number[]; // lines completed by this tap, as indices into LINES
228
+ totalLines: number; // lines completed so far
229
+ score: number; // score after this tap
230
+ }
231
+
232
+ interface PerfectEvent {
233
+ score: number; // final score, including the 1000-point bonus the first time
234
+ maxCombo: number;
235
+ }
236
+ ```
237
+
238
+ ### Ref handle
239
+
240
+ ```ts
241
+ interface PackingBingoHandle {
242
+ /** Same as the reset button: clears the card and score, and calls onChange. A controlled card only calls onChange. */
243
+ reset(): void;
244
+ /** A one-item change plays like a tap; bigger changes apply quietly. Doesn't call onChange. */
245
+ setChecked(checked: readonly boolean[]): void;
246
+ /** null before mount and on the server. */
247
+ getState(): PackingBingoState | null;
248
+ }
249
+
250
+ interface PackingBingoState {
251
+ checked: boolean[]; // 16 booleans, row by row
252
+ lines: number[]; // completed lines, as indices into LINES
253
+ score: number;
254
+ maxCombo: number;
255
+ complete: boolean; // all 16 checked
256
+ }
257
+ ```
258
+
259
+ `LINES` is exported. Its entries 0–3 are the rows, 4–7 the columns, 8 the diagonal from the top left, and 9 the diagonal from the top right.
260
+
261
+ ### Scoring
262
+
263
+ - An item checked for the first time scores 10 × the current combo. A combo is a chain of taps less than 2.5 s apart.
264
+ - Completing n lines with one tap scores 100 × n². Each line scores only once.
265
+ - Completing the card for the first time adds 1000.
266
+ - Unchecking never takes points away, and checking an item again earns nothing.
267
+ - After a quiet update or a rebuild (loading saved progress, for example), the score is recomputed as 10 per checked item plus 100 per completed line, and the max combo starts again from 0.
268
+
269
+ ### Messages
270
+
271
+ `messages` accepts any subset of these keys. The full Japanese and English sets are `ja` and `en` in [`src/messages.ts`](src/messages.ts).
272
+
273
+ - **Text:** `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`. In `perfectStamp` and `perfectBody`, `\n` starts a new line.
274
+ - **Functions:** `combo(n)`, `comboPop(n)`, `bannerNth(totalLines)`, `bannerMulti(newLines)`, `statusStart(left)`, `statusLines(lines, left)`, `statusReach(labels)`, `perfectBonus(points)`. Each returns a string.
275
+
276
+ ### Exports
277
+
278
+ - `packing-bingo`: `PackingBingo`, `createPackingBingo`, `LINES`, `DEFAULT_ITEMS`, `ja`, `en`, and the types `PackingBingoProps`, `PackingBingoHandle`, `PackingBingoState`, `BingoItem`, `BingoEvent`, `PerfectEvent`, `Messages`, `PackingBingoOptions`, `PackingBingoInstance`.
279
+ - `packing-bingo/core`: the same without the React component and its types (`PackingBingoProps`, `PackingBingoHandle`), so it doesn't import React.
280
+
281
+ ## Integration notes
282
+
283
+ ### What the card adds to your page
284
+
285
+ On first mount, the card appends `<style id="pkb-styles">` to `<head>`, shared by every card and left in place after unmount. Unless `fonts={false}`, it also appends `<link id="pkb-fonts">` for Google Fonts. All CSS is scoped under `.pkb`, and every class and keyframe name starts with `pkb-`, so the rest of your page is left alone.
286
+
287
+ Everything the card draws (the confetti canvas, the banner, the finale screen) stays inside the card's own box. Nothing is `position: fixed` or attached to `<body>`, and the card is its own stacking context, so a sticky site header stays on top of it.
288
+
289
+ ### Content Security Policy
290
+
291
+ The injected `<style>` and a few inline `style` attributes need `style-src 'unsafe-inline'`. With fonts on, also allow `https://fonts.googleapis.com` in `style-src` and `https://fonts.gstatic.com` in `font-src`.
292
+
293
+ ### Fonts
294
+
295
+ The card uses Dela Gothic One and M PLUS Rounded 1c. With `fonts={false}`, nothing is requested from Google. If your site already loads fonts with those family names (self-hosted, for example), the card uses them. Otherwise it falls back to `--pkb-font-fallback`.
296
+
297
+ ### Sound and vibration
298
+
299
+ Sound is synthesized with the Web Audio API. The audio context is only created after the user has interacted with the page, so the browser shows no autoplay warnings. The 🔊 button's choice is stored in localStorage as `packing-bingo-muted`, and every card created afterwards starts with it, including after a reload. Cards already on screen keep their own setting. Taps and bingos vibrate where the Vibration API exists, which in practice means Android browsers.
300
+
301
+ ### Saved data
302
+
303
+ With `storageKey`, the checks are stored under that key as a JSON array of 16 booleans. The score isn't stored. Saved progress takes priority over `defaultChecked`. If two cards on one page should keep separate progress, give each its own key. Blocked storage, as in some private browsing modes, is handled: the card just doesn't save.
304
+
305
+ ### Accessibility
306
+
307
+ Each square is a native checkbox inside a `<label>`, so Tab and Space work and the item name is the accessible name. The emoji and the stamp are hidden from assistive technology. The status line is a polite live region. With `prefers-reduced-motion`, shaking, bouncing and particles are turned off. It hasn't been tested with a screen reader yet.
308
+
309
+ ### Browser support and testing
310
+
311
+ The card uses CSS container queries (Chrome and Edge 105+, Safari 16+, Firefox 110+) and `:has()` for the keyboard focus ring (Firefox 121+).
312
+
313
+ Tested so far:
314
+
315
+ - React 19 in StrictMode, with three cards on one page, controlled and uncontrolled
316
+ - Server rendering with `renderToString`, ESM and CommonJS
317
+ - A Next.js 16 App Router production build, using the card from a Server Component page and from a client component
318
+ - Installing from npm with npm and pnpm
319
+ - Taps scripted in Chromium at desktop and phone sizes, including resets and callbacks that land mid-celebration
320
+
321
+ These scripted browser tests aren't part of this repository yet. Safari, Firefox and real phones haven't been tried, and the sound hasn't been checked on a real device. Emoji come from each device's emoji font, so they look different on each system.
322
+
323
+ ## Without React
324
+
325
+ The engine has no framework dependency:
326
+
327
+ ```ts
328
+ import { createPackingBingo } from "packing-bingo/core";
329
+
330
+ const card = createPackingBingo(document.getElementById("bingo")!, {
331
+ storageKey: "trip",
332
+ onBingo: (e) => console.log(e),
333
+ });
334
+ // card.reset(), card.getState(), card.setChecked(next, { animate: true }), card.destroy()
335
+ ```
336
+
337
+ Its options are the React props without `checked`, `className`, `style` and `ref` (type `PackingBingoOptions`). For a controlled card, pass `controlled: true` and update it with `setChecked()`. Without `{ animate: true }`, `setChecked()` always applies quietly.
338
+
339
+ Without a bundler, load the browser build from a CDN. It defines a global `PackingBingo`:
340
+
341
+ ```html
342
+ <div id="bingo"></div>
343
+ <script src="https://cdn.jsdelivr.net/npm/packing-bingo@0.1/dist/packing-bingo.global.js"></script>
344
+ <script>
345
+ PackingBingo.createPackingBingo(document.getElementById("bingo"), { storageKey: "trip" });
346
+ </script>
347
+ ```
348
+
349
+ ## Standalone page
350
+
351
+ `index.html` is the whole app in one file, so you can open it directly or put it on any static host. It saves progress in the browser.
352
+
353
+ ```sh
354
+ git clone https://github.com/Jin-hiroo/packing-bingo.git
355
+ cd packing-bingo
356
+ open index.html # macOS; on other systems, open the file in a browser
357
+ ```
358
+
359
+ Add `?demo` to the URL to start part-way through and show the demo buttons: "▶ ビンゴを見る" plays a bingo, and "▶ コンボ → パーフェクト" plays a combo run to a full card. On GitHub Pages, use **Settings → Pages → Deploy from a branch → `main` / root**.
360
+
361
+ To change the page, edit the options in its last `<script>`, which calls `PackingBingo.createPackingBingo(…)`. To change the defaults for every build, edit `src/items.ts` or `src/messages.ts` and run `npm run build`.
362
+
363
+ ## Development
364
+
365
+ ```sh
366
+ npm install # also builds dist/ and index.html
367
+ npm run build # dist/ (ESM, CJS, types, browser bundle) and the standalone index.html
368
+ npm run typecheck
369
+ ```
370
+
371
+ `index.html` is generated from `src/` by `scripts/build-standalone.mjs`, so don't edit it by hand.
372
+
373
+ ## 日本語
374
+
375
+ 修学旅行の持ち物チェックリストを4×4のビンゴにする、Reactのコンポーネントです。持ち物をかばんに入れるたびにマスをタップしてスタンプを押し、タテ・ヨコ・ナナメがそろうとビンゴになります。
376
+
377
+ ```sh
378
+ npm install packing-bingo
379
+ ```
380
+
381
+ ```tsx
382
+ import { PackingBingo } from "packing-bingo";
383
+
384
+ <PackingBingo storageKey="kyoto-trip-2026" />;
385
+ ```
386
+
387
+ - React 18以上で動きます。ほかに依存するパッケージはなく、CSSの読み込みも要りません。
388
+ - `storageKey` を付けると、チェックがその人のブラウザ(localStorage)に保存されます。
389
+ - 持ち物は `items`(ちょうど16個)で、文言は `messages` で変えられます。英語の文言は `en` です。
390
+ - 自分のサーバーに保存したいときは、`checked` と `onChange` を使って親側で状態を持ちます([Save progress on your server](#save-progress-on-your-server))。
391
+ - Next.jsのApp Routerでは、Server Componentのページにそのまま置けます(`items` や `messages={en}` も渡せます)。ただし、`onChange` などのコールバックは渡せないので、使うときは `"use client"` を付けた自分のコンポーネントで包んでください。
392
+ - 配色はCSS変数(`--pkb-sun` など)で変えられます。Google Fontsを読み込みたくないときは `fonts={false}` にします。
393
+ - CSP(Content Security Policy)を設定している場合は、`style-src 'unsafe-inline'` が必要です。
394
+
395
+ 詳しい仕様は [API](#api) と [Integration notes](#integration-notes) にあります。
396
+
397
+ ## License
398
+
399
+ [MIT](LICENSE) © 2026 Jin-hiroo.
400
+
401
+ The Dela Gothic One and M PLUS Rounded 1c fonts are loaded from Google Fonts and are licensed under the SIL Open Font License. They aren't included in this repository.