@minicode-dev/chat 0.1.13 → 0.1.15

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +112 -5
  2. package/README.md +401 -312
  3. package/assets/sounds/effects/avalanche.mp3 +0 -0
  4. package/assets/sounds/racing/car_boost.mp3 +0 -0
  5. package/assets/sounds/racing/car_boost.ogg +0 -0
  6. package/assets/sounds/racing/car_bump.mp3 +0 -0
  7. package/assets/sounds/racing/car_bump.ogg +0 -0
  8. package/assets/sounds/racing/car_drift_loop.mp3 +0 -0
  9. package/assets/sounds/racing/car_drift_loop.ogg +0 -0
  10. package/assets/sounds/racing/car_engine_loop.mp3 +0 -0
  11. package/assets/sounds/racing/car_engine_loop.ogg +0 -0
  12. package/assets/sounds/racing/car_engine_start.mp3 +0 -0
  13. package/assets/sounds/racing/car_engine_start.ogg +0 -0
  14. package/assets/sounds/racing/countdown_beep.mp3 +0 -0
  15. package/assets/sounds/racing/countdown_beep.ogg +0 -0
  16. package/assets/sounds/racing/countdown_go.mp3 +0 -0
  17. package/assets/sounds/racing/countdown_go.ogg +0 -0
  18. package/assets/sounds/racing/final_lap.mp3 +0 -0
  19. package/assets/sounds/racing/final_lap.ogg +0 -0
  20. package/assets/sounds/racing/finish_line.mp3 +0 -0
  21. package/assets/sounds/racing/finish_line.ogg +0 -0
  22. package/assets/sounds/racing/lap_complete.mp3 +0 -0
  23. package/assets/sounds/racing/lap_complete.ogg +0 -0
  24. package/assets/sounds/racing/music_circuit.mp3 +0 -0
  25. package/assets/sounds/racing/music_circuit.ogg +0 -0
  26. package/assets/sounds/racing/music_final_lap.mp3 +0 -0
  27. package/assets/sounds/racing/music_final_lap.ogg +0 -0
  28. package/assets/sounds/racing/race_defeat.mp3 +0 -0
  29. package/assets/sounds/racing/race_defeat.ogg +0 -0
  30. package/assets/sounds/racing/race_victory.mp3 +0 -0
  31. package/assets/sounds/racing/race_victory.ogg +0 -0
  32. package/assets/sounds/tanks/barrel_explode.mp3 +0 -0
  33. package/assets/sounds/tanks/barrel_explode.ogg +0 -0
  34. package/assets/sounds/tanks/crate_break.mp3 +0 -0
  35. package/assets/sounds/tanks/crate_break.ogg +0 -0
  36. package/assets/sounds/tanks/health_pickup.mp3 +0 -0
  37. package/assets/sounds/tanks/health_pickup.ogg +0 -0
  38. package/assets/sounds/tanks/last_life.mp3 +0 -0
  39. package/assets/sounds/tanks/last_life.ogg +0 -0
  40. package/assets/sounds/tanks/life_lost.mp3 +0 -0
  41. package/assets/sounds/tanks/life_lost.ogg +0 -0
  42. package/assets/sounds/tanks/respawn.mp3 +0 -0
  43. package/assets/sounds/tanks/respawn.ogg +0 -0
  44. package/assets/sounds/tanks/shield_end.mp3 +0 -0
  45. package/assets/sounds/tanks/shield_end.ogg +0 -0
  46. package/assets/sounds/tanks/shield_loop.mp3 +0 -0
  47. package/assets/sounds/tanks/shield_loop.ogg +0 -0
  48. package/assets/sounds/tanks/wall_hit.mp3 +0 -0
  49. package/assets/sounds/tanks/wall_hit.ogg +0 -0
  50. package/dist/chunks/{BattleshipGameCard-iRk8d4vR.js → BattleshipGameCard-8H-sBCsv.js} +146 -144
  51. package/dist/chunks/{BattleshipGameCard-iRk8d4vR.js.map → BattleshipGameCard-8H-sBCsv.js.map} +1 -1
  52. package/dist/chunks/{ChatContext-DjIEifGY.js → ChatContext-CVW58bzC.js} +2 -2
  53. package/dist/chunks/{ChatContext-DjIEifGY.js.map → ChatContext-CVW58bzC.js.map} +1 -1
  54. package/dist/chunks/RacingLiveGameCard-8j9QLnxW.js +1654 -0
  55. package/dist/chunks/RacingLiveGameCard-8j9QLnxW.js.map +1 -0
  56. package/dist/chunks/{TanksGameCard-GrqmdwJ7.js → TanksGameCard-DnCufSH6.js} +168 -166
  57. package/dist/chunks/{TanksGameCard-GrqmdwJ7.js.map → TanksGameCard-DnCufSH6.js.map} +1 -1
  58. package/dist/chunks/TanksLiveGameCard-D_27Jomp.js +1274 -0
  59. package/dist/chunks/TanksLiveGameCard-D_27Jomp.js.map +1 -0
  60. package/dist/chunks/avalancheScene3d-CIE2WKQn.js +1300 -0
  61. package/dist/chunks/avalancheScene3d-CIE2WKQn.js.map +1 -0
  62. package/dist/chunks/cars-D1PM7mVD.js +340 -0
  63. package/dist/chunks/cars-D1PM7mVD.js.map +1 -0
  64. package/dist/chunks/connect4Sounds-BqEHULjv.js +235 -0
  65. package/dist/chunks/connect4Sounds-BqEHULjv.js.map +1 -0
  66. package/dist/chunks/connectionStore-DwgIUl30.js +13 -0
  67. package/dist/chunks/connectionStore-DwgIUl30.js.map +1 -0
  68. package/dist/chunks/createLiveScene-MTGRv8rx.js +758 -0
  69. package/dist/chunks/createLiveScene-MTGRv8rx.js.map +1 -0
  70. package/dist/chunks/createRaceScene-BiEtQsVs.js +306 -0
  71. package/dist/chunks/createRaceScene-BiEtQsVs.js.map +1 -0
  72. package/dist/chunks/{createTankScene-6asWHKFf.js → createTankScene-yhX6la27.js} +125 -123
  73. package/dist/chunks/{createTankScene-6asWHKFf.js.map → createTankScene-yhX6la27.js.map} +1 -1
  74. package/dist/chunks/decor-CyyCnmct.js +418 -0
  75. package/dist/chunks/decor-CyyCnmct.js.map +1 -0
  76. package/dist/chunks/fx-1ZwzmPWj.js +458 -0
  77. package/dist/chunks/fx-1ZwzmPWj.js.map +1 -0
  78. package/dist/chunks/fx-DhXerGyt.js +248 -0
  79. package/dist/chunks/fx-DhXerGyt.js.map +1 -0
  80. package/dist/chunks/gameStatus-BChd29yI.js +62 -0
  81. package/dist/chunks/gameStatus-BChd29yI.js.map +1 -0
  82. package/dist/chunks/games-Bl_8raG6.js +3681 -0
  83. package/dist/chunks/games-Bl_8raG6.js.map +1 -0
  84. package/dist/chunks/rolldown-runtime-CGnMp29N.js +10 -0
  85. package/dist/chunks/tank-DjEQ4FXF.js +553 -0
  86. package/dist/chunks/tank-DjEQ4FXF.js.map +1 -0
  87. package/dist/chunks/tanks-live-BcKmrhnV.js +261 -0
  88. package/dist/chunks/tanks-live-BcKmrhnV.js.map +1 -0
  89. package/dist/chunks/{tanksSounds-CUNDIOal.js → tanksSounds-DTnidbiq.js} +44 -12
  90. package/dist/chunks/tanksSounds-DTnidbiq.js.map +1 -0
  91. package/dist/chunks/track-BcnnpnAm.js +259 -0
  92. package/dist/chunks/track-BcnnpnAm.js.map +1 -0
  93. package/dist/chunks/util-DwI9bfS7.js +57 -0
  94. package/dist/chunks/util-DwI9bfS7.js.map +1 -0
  95. package/dist/index.js +822 -635
  96. package/dist/index.js.map +1 -1
  97. package/dist/styles.css +1 -1
  98. package/index.d.ts +3 -1
  99. package/package.json +1 -1
  100. package/dist/chunks/TanksLiveGameCard-DSmQcUMh.js +0 -1225
  101. package/dist/chunks/TanksLiveGameCard-DSmQcUMh.js.map +0 -1
  102. package/dist/chunks/avalancheScene3d-Dnl2E3EA.js +0 -1878
  103. package/dist/chunks/avalancheScene3d-Dnl2E3EA.js.map +0 -1
  104. package/dist/chunks/connect4Sounds-Cv0q31VW.js +0 -3245
  105. package/dist/chunks/connect4Sounds-Cv0q31VW.js.map +0 -1
  106. package/dist/chunks/createLiveScene-819NWv84.js +0 -365
  107. package/dist/chunks/createLiveScene-819NWv84.js.map +0 -1
  108. package/dist/chunks/fx-BNJvJYOu.js +0 -871
  109. package/dist/chunks/fx-BNJvJYOu.js.map +0 -1
  110. package/dist/chunks/gameStatus-Cu1Z8un9.js +0 -69
  111. package/dist/chunks/gameStatus-Cu1Z8un9.js.map +0 -1
  112. package/dist/chunks/tank-DIn6Jyt8.js +0 -605
  113. package/dist/chunks/tank-DIn6Jyt8.js.map +0 -1
  114. package/dist/chunks/tanksSounds-CUNDIOal.js.map +0 -1
package/README.md CHANGED
@@ -1,59 +1,86 @@
1
1
  # @minicode-dev/chat
2
2
 
3
- Real-time chat for React (WebSocket, reactions, editing, emoji, GIFs/stickers via KLIPY, file and voice
4
- attachments, notifications, Connect 4 and 3D Battleship).
3
+ Real-time chat for React: WebSocket transport, reactions, editing, replies, emoji, GIFs/stickers via KLIPY,
4
+ file and voice attachments, YouTube players, notifications, animated effects and four two-player games.
5
5
 
6
- Standalone, reusable npm package for integration into React applications through configurable
7
- providers and transport adapters.
6
+ A standalone, reusable npm package. The host application supplies the transport, authentication and served
7
+ assets through configuration; nothing is tied to a specific backend or application.
8
8
 
9
- - Owner: **Minicode** · License: `UNLICENSED`.
9
+ - Owner: **Minicode** · License: `UNLICENSED` · Public on npm (`publishConfig.access: public`).
10
+ - The UI copy is in Spanish.
10
11
 
11
- ## Installation
12
+ ## Features
12
13
 
13
- Install the package from npm. If your registry requires authentication, configure an `.npmrc` in
14
- the host project (keep the token out of git, e.g. via an environment variable):
14
+ | Feature | What you get | Host requirements |
15
+ |---|---|---|
16
+ | Messaging | Private chats and groups, replies, editing, reactions, presence, user search, unread badges | WebSocket backend speaking the chat protocol (see [Wire protocol](#wire-protocol)) |
17
+ | Emoji | Unicode picker (English/Spanish search, recents, favorites, skin tones), `:shortcode` autocomplete, emoticons such as `:)` and `<3` | Bundler with JSON `import()` support |
18
+ | Custom emoji | Image emojis per provider in messages, picker, autocomplete and reactions | `media.emoji.custom` catalog and image hosting |
19
+ | GIFs and stickers | KLIPY search, categories, local favorites/recents | `configureKlipy({ apiKey })` and an attachment-capable `sendMessage` |
20
+ | Attachments and voice notes | Files up to 10 MB, in-browser voice recording | Attachment-capable `sendMessage`; microphone permission |
21
+ | YouTube | Inline player for the first supported link per message | CSP `frame-src https://www.youtube-nocookie.com` |
22
+ | Effects | Zumbido, Cerdito disco, Confeti, Avalancha and countdown messages | Served sounds; optional `three` |
23
+ | Games | Conecta 4, Batalla Naval 3D, Tanques de guerra (turn-based), Tanques LIVE 3D (WebRTC) | Games transport bridge; served sounds; optional `three` |
24
+ | Notifications and PWA | Desktop notifications, app badge, message sounds, detached window, install prompt | `<ChatNotifications />` or the `start*` services; your own `/chat` route |
25
+ | Theming | `light`, `dark`, `auto`, `black` | `theme` prop, or the host's `body.dark-mode` / `[data-theme]` |
15
26
 
16
- ```ini
17
- @minicode-dev:registry=https://registry.npmjs.org/
18
- //registry.npmjs.org/:_authToken=${NPM_TOKEN}
19
- ```
27
+ ## Installation
20
28
 
21
29
  ```bash
22
30
  npm install @minicode-dev/chat react react-dom zustand lucide-react
23
- # Only if Battleship 3D is enabled:
24
- npm install three@">=0.134.0 <0.138.0"
31
+ # Optional, only for the 3D scenes (games and effects fall back without it):
32
+ npm install "three@>=0.134.0 <0.138.0"
25
33
  ```
26
34
 
27
- ### Peer dependencies
28
-
29
- | Package | Range | Notes |
35
+ | Peer | Range | Notes |
30
36
  |---|---|---|
31
37
  | `react`, `react-dom` | `>=18` | |
32
38
  | `zustand` | `^4.5.0 \|\| ^5.0.0` | |
33
39
  | `lucide-react` | `>=0.400.0` | |
34
- | `three` | `>=0.134.0 <0.138.0` | Optional, only for Battleship 3D (loaded lazily). |
40
+ | `three` | `>=0.134.0 <0.138.0` | Optional. Loaded with dynamic `import()` only when a 3D scene starts. |
41
+
42
+ `dayjs` and `emojibase-data` are regular dependencies. The emoji catalog (~1.4 MB of JSON) is loaded with dynamic
43
+ `import()` the first time it is needed, so the host bundler must support JSON imports (Vite, webpack 5,
44
+ esbuild, Rollup with `@rollup/plugin-json`).
35
45
 
36
- `emojibase-data` and `dayjs` are regular dependencies. The emoji catalog (~1.4 MB of JSON) is loaded with
37
- dynamic `import()` the first time the composer/emoji picker mounts, so the host bundler must support
38
- JSON imports (Vite, webpack 5, Rollup with `@rollup/plugin-json`, esbuild do).
46
+ The package is ESM only (`dist/index.js`). JavaScript does not import the stylesheet; import it yourself.
39
47
 
40
- ## Usage
48
+ ## Quick start
41
49
 
42
50
  ```jsx
51
+ import { useEffect } from 'react';
43
52
  import {
44
53
  LiraChatProvider, ChatButton, ChatWindow, ChatPage, ChatNotifications,
54
+ chatGamesStore, useChatTransport,
45
55
  } from '@minicode-dev/chat';
46
56
  import '@minicode-dev/chat/styles.css';
47
57
 
48
58
  const config = {
49
59
  websocket: { url: 'wss://chat.example.com/ws' },
50
60
  auth: { getToken: () => localStorage.getItem('token') },
51
- callbacks: { onError: (error) => console.error(error) },
61
+ user: { id: 'user-123', name: 'Ana' },
62
+ callbacks: { onConnectionChange: (status) => console.info('chat', status) },
52
63
  };
53
64
 
65
+ // Games travel as chat text: give the games store a way to send it.
66
+ function ChatGamesBridge() {
67
+ const transport = useChatTransport();
68
+ useEffect(() => {
69
+ if (!transport) return undefined;
70
+ chatGamesStore.getState().configureTransport({
71
+ send: (roomId, text) => transport.request('CHAT_MESSAGE', { room_id: roomId, message: text }),
72
+ getScope: () => transport.getIdentity().scope,
73
+ getUserId: () => transport.getIdentity().userId,
74
+ });
75
+ return () => chatGamesStore.getState().configureTransport(null);
76
+ }, [transport]);
77
+ return null;
78
+ }
79
+
54
80
  export function App() {
55
81
  return (
56
82
  <LiraChatProvider config={config}>
83
+ <ChatGamesBridge />
57
84
  <ChatButton label="Chat" />
58
85
  <ChatWindow placement="floating" />
59
86
  <ChatNotifications />
@@ -61,136 +88,162 @@ export function App() {
61
88
  );
62
89
  }
63
90
 
64
- // Full-page route (e.g. /chat, also used by the "detach" window):
91
+ // Full-page route (default `/chat`), also opened by the "detach" button:
65
92
  export function ChatRoute() {
66
93
  return (
67
94
  <LiraChatProvider config={config}>
95
+ <ChatGamesBridge />
68
96
  <ChatPage />
69
97
  </LiraChatProvider>
70
98
  );
71
99
  }
72
100
  ```
73
101
 
74
- - `LiraChatProvider` props: `config`, `transport` (inject your own `ChatTransport` adapter;
75
- the provider never closes an injected transport), `stores`, `chatStore` and `children`. Without a
76
- `transport`, a standalone WebSocket transport is created from `config.websocket.url`.
77
- - `ChatWindow` props: `placement` (`'floating' | 'modal' | 'embedded'`), `open` / `onOpenChange`
78
- (controlled mode), `className`, `BattleshipPreviewComponent`.
79
- - `ChatPage`: `className`. `ChatButton`: `className`, `label`. `ChatNotifications`: no props.
80
- - Lower level: `createStandaloneTransport`, `createChatStore`, hooks (`useChatConfig`, `useChatTransport`,
81
- `useChatConnection`, `useChatUnreadTotal`) and presentational primitives (`ChatWindowShell`,
82
- `MessageList`, `MessageComposer`, pickers…). See `index.d.ts` for the full typed API; anything not
83
- exported from the package root is internal.
84
-
85
- ## Styles
86
-
87
- Import the stylesheet once, anywhere in the host app:
88
-
89
- ```js
90
- import '@minicode-dev/chat/styles.css';
91
- ```
92
-
93
- ## Assets (sounds)
102
+ Then serve the sounds ([Assets](#assets-sounds)). Without the games bridge, game invitations fail with
103
+ "Chat no disponible". The provider creates its transport and chat store once; remount it with a different
104
+ `key` to change `websocket` or `auth`.
94
105
 
95
- Notification and game sounds ship in `assets/sounds/` (`ting.*`, `notification.*`, `connect4/*.ogg`,
96
- `battleship_audio_pack/assets/*.ogg`) but **the host serves them**. By default the chat requests them
97
- from `/sounds/...`. Either:
106
+ ## Host integration checklist
98
107
 
99
- 1. Copy them into the host's public folder, e.g. in a `postinstall`/build step:
100
-
101
- ```bash
102
- mkdir -p public/sounds
103
- cp -R node_modules/@minicode-dev/chat/assets/sounds/. public/sounds/
104
- ```
105
-
106
- 2. Or serve them from another path/CDN and point the chat at it, before any sound plays:
108
+ 1. **Styles:** `import '@minicode-dev/chat/styles.css'` once.
109
+ 2. **Sounds:** copy `assets/sounds/` to `/sounds/` or call `setChatAssetsBaseUrl()` ([Assets](#assets-sounds)).
110
+ 3. **Games:** call `chatGamesStore.getState().configureTransport({ send, getScope, getUserId })` (see Quick start).
111
+ Games only run in private rooms named `private_<userA>_<userB>`.
112
+ 4. **Attachments, GIFs, stickers and voice notes** are all sent as attachments. The default store sends text
113
+ only and rejects attachments ("Este transporte no admite adjuntos."). Create the store yourself with an
114
+ attachment-capable `sendMessage(roomId, message, attachments)` and pass it as `chatStore`:
107
115
 
108
116
  ```js
109
- import { setChatAssetsBaseUrl } from '@minicode-dev/chat';
110
- setChatAssetsBaseUrl('https://cdn.example.com/minicode-chat/sounds');
117
+ import { createChatStore } from '@minicode-dev/chat';
118
+ const chatStore = createChatStore({ transport, sendMessage: mySendMessage });
119
+ // <LiraChatProvider config={config} transport={transport} chatStore={chatStore}>
111
120
  ```
112
121
 
113
- (`config.assets.baseUrl` exists in the config schema but is not wired yet; use the function.)
114
-
115
- The files are also exposed as `@minicode-dev/chat/assets/*` for bundlers that can import asset URLs.
122
+ Hosts that own a raw WebSocket can use the exported file relay: `sendRelayMessage(socket, roomId, message,
123
+ attachments)` sends files as 32 KB `CHAT_FILE` frames (one attachment per message, 1 byte to 10 MB);
124
+ `consumeRelayEvent(socket, frame)` and `hydrateRelayMessage(socket, message)` rebuild them on receipt;
125
+ `closeRelay(socket)` rejects pending sends. File bytes only live in connected browsers.
126
+ 5. **KLIPY:** `configureKlipy({ apiKey })` before the GIF/sticker pickers open ([GIFs](#gifs-and-stickers)).
127
+ 6. **Detached window / PWA:** render `<ChatPage />` at `/chat` (or set `pwa.path`). For the installable app,
128
+ call `startChatApp()` and `registerChatWorker({ path: '/chat', swUrl: '/chat-sw.js' })`; the service worker
129
+ file is yours, the package does not ship one.
130
+ 7. **Background services:** `<ChatNotifications />` starts desktop notifications, the app badge and message
131
+ and Connecta 4 sounds while mounted. Hosts can instead start `startDesktopNotifications()`,
132
+ `startAppBadge()` and `startConnect4Sounds()`. Cross-tab sync of unread counts, GIF library and emoji
133
+ preferences: `startUnreadStorageSync()`, `startGifLibrarySync()`, `startEmojiPreferencesSync()`. Every
134
+ starter returns `stop()`.
116
135
 
117
- ## Local development
118
-
119
- This repository includes a local React demo that consumes the library source directly:
136
+ ## Assets (sounds)
120
137
 
121
- ```bash
122
- npm install
123
- npm run dev
124
- ```
138
+ Sounds ship in the package under `assets/sounds/` (also exported as `@minicode-dev/chat/assets/*`), but **the
139
+ host serves them**. By default the chat requests them from `/sounds/...`.
125
140
 
126
- Vite opens the chat in your browser. The demo provides simulated users, automatic replies,
127
- groups, emoji, reactions, message editing, attachments and a light/dark theme switch.
128
- Messages and uploaded files stay in memory and reset on reload. No login or WebSocket
129
- service is required. GIF/sticker search requires separately configured KLIPY credentials;
130
- multiplayer games are not simulated. Voice recording requires browser microphone permission.
131
- The `demo/` directory is excluded from the npm package by the `files` allowlist.
141
+ | Path in `assets/sounds/` | Used by |
142
+ |---|---|
143
+ | `ting.mp3`/`.ogg`, `notification.mp3`/`.ogg` | New-message sounds |
144
+ | `effects/nudge.mp3`, `cerdito_disco_original.mp3`, `confetti.mp3`, `avalanche.mp3` | Zumbido, Cerdito disco, Confeti, Avalancha |
145
+ | `connect4/*.ogg` (8 files) | Conecta 4 |
146
+ | `battleship_audio_pack/assets/*.ogg` (14 files) | Batalla Naval |
147
+ | `tanks/*.mp3` and `tanks/*.ogg` (12 sounds, both formats) | Tanques de guerra and Tanques LIVE |
132
148
 
133
- To build or pack the library:
149
+ Option 1, copy the folder into the host's public directory (Vite/CRA `public/` is served at `/`):
134
150
 
135
151
  ```bash
136
- npm install
137
- npm run build # ESM library and styles in dist/
138
- npm pack # builds again via prepack and creates an installable .tgz
152
+ mkdir -p public/sounds
153
+ cp -R node_modules/@minicode-dev/chat/assets/sounds/. public/sounds/
139
154
  ```
140
155
 
141
- To test local changes, install the generated tarball in a React application:
156
+ Automate it so new sounds arrive with every package update. A cross-platform script (Node 16.7+):
142
157
 
143
- ```bash
144
- # Run inside your consuming application; adjust the path and package version.
145
- npm install /path/to/minicode-chat/minicode-dev-chat-0.1.0.tgz
146
- npm run dev # if your application provides this script
158
+ ```js
159
+ // scripts/copy-chat-sounds.mjs
160
+ import { cpSync, mkdirSync } from 'node:fs';
161
+ import { createRequire } from 'node:module';
162
+ import { dirname, join } from 'node:path';
163
+
164
+ const require = createRequire(import.meta.url);
165
+ const packageDir = dirname(require.resolve('@minicode-dev/chat/package.json'));
166
+ const target = join(process.cwd(), 'public', 'sounds');
167
+
168
+ mkdirSync(target, { recursive: true });
169
+ cpSync(join(packageDir, 'assets', 'sounds'), target, { recursive: true });
170
+ console.log(`@minicode-dev/chat sounds copied to ${target}`);
147
171
  ```
148
172
 
149
- Repeat packing and installation after changes. Installation updates the consuming application's
150
- local dependency files. Real-time messaging requires a compatible WebSocket service and any
151
- necessary authentication, supplied by the consuming application.
152
-
153
- For source-based development, the custom `@minicode-dev/source` export condition points at `src/`.
154
- Tooling must explicitly enable it (for example through Vite's `resolve.conditions`) and resolve
155
- this local package. It is not enabled by default; React and other shared peers must resolve to
156
- one copy in the consuming application.
157
-
158
- ## Publishing updates
173
+ ```json
174
+ {
175
+ "scripts": {
176
+ "copy:chat-sounds": "node scripts/copy-chat-sounds.mjs",
177
+ "predev": "npm run copy:chat-sounds",
178
+ "prebuild": "npm run copy:chat-sounds"
179
+ }
180
+ }
181
+ ```
159
182
 
160
- Log in yourself with `npm login`, then run:
183
+ Option 2, serve them from another path or CDN and point the chat there before any sound plays:
161
184
 
162
- ```bash
163
- npm run release # patch version, build, validate contents, publish
164
- npm run release -- minor # minor version
165
- npm run release -- major # major version
166
- npm run release -- --dry-run # build and inspect; no version change or publication
185
+ ```js
186
+ import { setChatAssetsBaseUrl } from '@minicode-dev/chat';
187
+ setChatAssetsBaseUrl('https://cdn.example.com/minicode-chat/sounds');
167
188
  ```
168
189
 
169
- The release script checks your npm session, builds, increments the version without
170
- creating a Git commit or tag, and validates the exact archive before publishing it.
171
- Only `dist/`, `assets/`, public types, README, changelog and package metadata are allowed.
172
- Temporary archives and this package's root `.tgz` files are removed even on failure.
173
- Build output in `dist/` is retained for local use. If publication fails after the
174
- version bump, the updated version remains in `package.json` and the lockfile.
175
- Registry access follows `publishConfig` in `package.json`; login and any required
176
- npm authentication challenge are handled by you. No Git push is performed.
190
+ `setChatAssetsBaseUrl(null)` restores `/sounds`. `config.assets.baseUrl` exists in the schema but is not wired;
191
+ use the function. Missing files only silence the sound; visuals and games keep working.
177
192
 
178
- The command is named `release` because npm reserves `publish` as a lifecycle hook;
179
- a script with that name that calls `npm publish` could invoke itself recursively.
193
+ ## Components and API
180
194
 
181
- ## Feature implementation notes
195
+ | Export | Notes |
196
+ |---|---|
197
+ | `LiraChatProvider` | Props: `config`, `transport` (your own `ChatTransport`; never connected or closed by the provider), `stores`, `chatStore`, `children`. Without `transport`, a standalone WebSocket transport is created from `config.websocket.url`. |
198
+ | `ChatWindow` | `placement`: `'floating'` (portal; minimize, maximize, detach; Escape minimizes), `'modal'` (backdrop and focus trap) or `'embedded'` (inline, always open). Also `open`/`onOpenChange` (controlled), `theme`, `className`, `BattleshipPreviewComponent`. |
199
+ | `ChatPage` | Full-page chat: `theme`, `className`, `BattleshipPreviewComponent`. |
200
+ | `ChatButton` | Launcher with unread badge; works outside the provider. `label`, `className`, `onClick`, `pressed`, `badgeClassName`, `iconSize`. Without `onClick` it toggles `chatUiStore`. |
201
+ | `ChatNotifications` | Renders nothing; runs notification services while mounted (honors `config.notifications`). |
202
+ | Hooks | `useChatConfig`, `useChatTransport`, `useChatConnection`, `useChatIdentity`, `useChatStore`, `useChatStoreApi`, `useChatUnreadTotal`, `useChatUi`, `useChatUnread`, `useChatGames`, `useChatInstall`, `useChatConnectionStatus` |
203
+ | Transport and stores | `createStandaloneTransport`, `assertTransport`, `TRANSPORT_STATUSES`, `createChatStore`, `MAX_CHAT_MESSAGES`, `chatUiStore`, `chatGamesStore`, `chatConnectionStore`, `normalizeConfig`, `DEFAULT_CONFIG` |
204
+ | Protocol helpers | `encodeEdit`, `encodeReaction`, `parseMessageAction`, `isMessageAction`, `groupReactions`, `isStickerAttachment`, `encodeEffect`, `decodeEffect`, `EFFECT_CATALOG`, `getChatEffect` |
205
+ | WebRTC | `createPeerLink`, `createChatSignaling`, `rtcSignals`, `withSignalFaults`, `DEFAULT_ICE_SERVERS`, `WebRtcLab` (lazy, render inside `<Suspense>`), `encodeSignal`, `decodeSignal`, `isWebrtcMessage`, `WEBRTC_PREFIX`, `MAX_SIGNAL_LENGTH` |
206
+ | Presentational | `ChatWindowShell`, `MessageList`, `MessageBubble`, `MessageComposer`, `EmojiPicker`, `GifPicker`, `StickerPicker`, `ChatPeopleContext` |
182
207
 
183
- For AI handoff and troubleshooting context, see [docs/contexto-para-ia.md](docs/contexto-para-ia.md)
184
- in the source repository (not included in the npm archive).
208
+ `index.d.ts` has the full typed API. Anything not exported from the package root is internal.
185
209
 
186
- For the Spanish implementation record and demo walkthrough, see [docs/funciones/README.md](docs/funciones/README.md)
187
- in the source repository. This companion document is not included in the npm archive;
188
- the integration sections below are included.
210
+ ## Configuration
189
211
 
190
- ## Custom image emojis
212
+ `normalizeConfig` shallow-merges each section over `DEFAULT_CONFIG`. Keys that take effect today:
191
213
 
192
- Provide a catalog per chat provider. Custom shortcodes work in messages, the emoji picker,
193
- autocomplete and reactions. Unicode emojis continue to work normally.
214
+ | Key | Default | Notes |
215
+ |---|---|---|
216
+ | `websocket.url` | `null` | String or (async) function. Used only when no `transport` is injected. |
217
+ | `websocket.reconnect` | `{ max: 3, baseMs: 1000, capMs: 10000 }` | Reconnect backoff. |
218
+ | `websocket.ping` | `false` | `{ action?, everyMs }` keep-alive. |
219
+ | `auth.getToken`, `auth.tokenParam` | `null`, `'token'` | Token added to the WebSocket URL query. |
220
+ | `user`, `scope` | `null` | User `{ id \| user_id \| sub, name \| username \| email }`; scope defaults to the user id. |
221
+ | `features.youtube` | `true` | `false` disables players. |
222
+ | `features.games.connect4`, `.battleship` | `true` | Set each to `false` to hide it. |
223
+ | `features.games.tanks`, `.tanksLive` | `false` | Opt in. |
224
+ | `features.detach` | `{ path: '/chat', windowName: 'lira-independent-chat' }` | `false` hides the detach button. `pwa.path`/`pwa.windowName` override it. |
225
+ | `media.emoji.custom` | `[]` | [Custom emoji](#custom-image-emojis). |
226
+ | `effects` | see [Effects](#effects) | `false` disables effects. |
227
+ | `notifications.desktop`, `.sound`, `.appBadge` | `true` | Read by `<ChatNotifications />`; `notifications: false` disables all three. |
228
+ | `webrtc.iceServers`, `.iceTransportPolicy` | `undefined`, `'all'` | Tanques LIVE STUN/TURN. |
229
+ | `portalContainer` | `document.body` | Element or function for portals. |
230
+ | `callbacks.onOpenChange`, `.onConnectionChange` | no-op | |
231
+
232
+ Accepted and typed but **not wired yet** (no effect): `theme`, `locale`, `storage`, `assets.baseUrl`,
233
+ `media.klipy`, `media.emoji.lazy`, `media.maxAttachmentBytes`, `media.trustedMediaHosts`,
234
+ `notifications.previews` (users toggle previews in the chat header), `websocket.legacyRoomIdRouting`,
235
+ `features.games.battleshipPreview`, the `features` flags `reactions`, `editing`, `replies`, `files`, `voice`,
236
+ `gifs`, `stickers`, `presence`, `groups`, `privateChats` and `install`, and `callbacks.onError`,
237
+ `onOpenConversation` and `onUnreadChange`. `features.games: false` and `features: false` currently do **not**
238
+ hide Conecta 4 or Batalla Naval; disable them with the individual flags.
239
+
240
+ ## Emoji
241
+
242
+ - Type `:` plus a name (for example `:smile`) for suggestions; an exact shortcode followed by `:` or a space
243
+ is converted. Classic emoticons (`:)`, `:D`, `;)`, `<3`…) are suggested as their emoji.
244
+ - Recents, favorites and skin tone are stored locally per browser.
245
+
246
+ ### Custom image emojis
194
247
 
195
248
  ```jsx
196
249
  const config = {
@@ -204,44 +257,50 @@ const config = {
204
257
  };
205
258
  ```
206
259
 
207
- Use lowercase names of 1–32 letters, numbers, `_`, `+` or `-`. The catalog accepts up to
208
- 100 entries. Unknown shortcodes or failed images remain readable as `:team_party:`.
209
- The consuming application supplies a shared catalog and image URLs for real users;
210
- it is responsible for upload permissions, storage and preserving images used in history.
211
- The local demo includes a session-only image uploader (PNG/JPG/WebP/GIF, up to 256 KB
212
- and 2048 × 2048) and an example image. Open **Nuevas funciones** to try it.
213
-
214
- ## Countdown messages
215
-
216
- Open **✨ → Cuenta regresiva**, choose a quick duration button or a custom duration in seconds or minutes
217
- (up to 24 hours), select the final effect (pig, confetti, avalanche or nudge), and choose
218
- whether to include its sound. **Probar efecto** previews the selection without sending.
219
- Send it to display a live countdown in the conversation. The card hides the chosen
220
- effect until zero; quotes and conversation previews also keep the surprise.
221
- The last ten seconds animate; at zero, the visible conversation plays the selected
222
- effect, respecting automatic playback, sound preferences and reduced motion.
223
-
224
- Countdowns use ordinary text messages with a separate `[minicode-countdown:v1]`
225
- payload containing an absolute deadline. No backend change is needed. Both clients
226
- need this library version for the countdown UI; older clients show readable fallback
227
- text. Device clocks should be accurate. Closed or hidden conversations do not play
228
- the deadline effect; expired cards offer manual replay on return.
229
-
230
- ## Zumbidos and animated effects
231
-
232
- The chat includes a nudge (Zumbido), a dancing pig (Cerdito disco), confetti (Confeti) and
233
- an avalanche (Avalancha 🚀: a 3D shower of money, cars, tanks and confetti).
234
- The pig uses `cerdito_disco_original.mp3`; nudge and confetti use `nudge.mp3` and `confetti.mp3`
235
- in the same `assets/sounds/effects/` directory. The avalanche uses `avalanche.mp3` from the same folder.
236
- Confetti and avalanche render with the optional `three` peer (loaded lazily) and fall back to CSS
237
- without `three`/WebGL or with reduced motion. Serve
238
- `assets/sounds/effects/cerdito_disco_original.mp3` at `/sounds/effects/cerdito_disco_original.mp3`,
239
- or use the existing `setChatAssetsBaseUrl` sound asset configuration. Copying the complete
240
- `assets/sounds/` directory as described above includes this recording.
241
- Open the **Efectos y zumbidos** button in the composer and click an effect to send it
242
- and play its animation. Each compact row shows sending/sent feedback; sound and automatic
243
- playback are controlled by two switches. Received
244
- effects appear as cards with manual replay; new received effects play automatically by default in the visible conversation.
260
+ Shortcodes are lowercase, 1–32 characters of letters, numbers, `_`, `+` or `-`; up to 100 entries; duplicates
261
+ and entries without an HTTP(S), relative or raster `data:` URL are ignored. Unknown shortcodes or failed images
262
+ stay readable as `:team_party:`. The host supplies the shared catalog and owns upload permissions, storage and
263
+ keeping images used in history.
264
+
265
+ ## GIFs and stickers
266
+
267
+ ```js
268
+ import { configureKlipy } from '@minicode-dev/chat';
269
+ configureKlipy({ apiKey: yourKlipyAppKey }); // from your own config; never commit it
270
+ ```
271
+
272
+ The key is sent from the browser in KLIPY request URLs. Optional `locale` (country code, default `do`) and
273
+ `categoriesLocale` (default `es_DO`). Without a key, the pickers load nothing. Only media from KLIPY's hosts is
274
+ accepted. Favorites and recents are local to the browser. GIFs and stickers are sent as attachments (see the
275
+ checklist).
276
+
277
+ ## Attachments and voice notes
278
+
279
+ Files from 1 byte to 10 MB, paste or pick; voice notes use `MediaRecorder` and need microphone permission
280
+ (HTTPS or localhost). Both require an attachment-capable `sendMessage` (see the checklist).
281
+
282
+ ## YouTube
283
+
284
+ Supported links: `watch?v=`, `youtu.be`, Shorts, Live and embed URLs (including start times), on
285
+ `youtube.com`, `m.youtube.com`, `music.youtube.com` and `youtube-nocookie.com`. The first video per message is
286
+ embedded from `youtube-nocookie.com`, lazy-loaded and without autoplay; the original text and an
287
+ **Abrir en YouTube** link stay visible. No API key is needed. Videos must allow embedding.
288
+
289
+ ## Effects
290
+
291
+ Open **✨ Efectos y zumbidos** in the composer and click an effect to send it. Allowed in private chats
292
+ (and in groups with `allowGroups: true`).
293
+
294
+ | ID | Name | Duration | Visual | Sound (`effects/`) |
295
+ |---|---|---|---|---|
296
+ | `nudge` | Zumbido ⚡ | 7 s | 3D thunderstorm, lightning and shake; Canvas 2D fallback | `nudge.mp3` |
297
+ | `pig-disco` | Cerdito disco 🐷 | 5.2 s | Dancing SVG pig with disco ball | `cerdito_disco_original.mp3` |
298
+ | `confetti` | Confeti 🎉 | 7 s | 3D party (confetti, balloons, gifts, streamers); 220-particle CSS fallback | `confetti.mp3` |
299
+ | `avalanche` | Avalancha 🚀 | 7 s | 3D shower of money, cars, tanks and confetti; CSS fallback | `avalanche.mp3` |
300
+
301
+ 3D scenes load `three` lazily and sync with the recording. Without `three`/WebGL or after a lost WebGL context
302
+ they use the fallback. With reduced motion, Confeti and Avalancha use the CSS fallback, Zumbido drops the shake
303
+ and dims the flashes, and CSS animations stop.
245
304
 
246
305
  ```jsx
247
306
  const config = {
@@ -259,115 +318,90 @@ const config = {
259
318
  };
260
319
  ```
261
320
 
262
- Effects use ordinary versioned text messages over the existing transport; servers must
263
- preserve the message text. `encodeEffect`, `decodeEffect`, `EFFECT_PREFIX`, `EFFECT_CATALOG`
264
- and `getChatEffect` are exported for integrations. Older clients show a readable fallback
265
- alongside the versioned payload. Set `effects: false` to disable the feature.
266
-
267
- Audio requires browser permission to play after a user gesture and respects the chat's
268
- mute control. Effects stop when closed or when leaving the conversation; motion reduction
269
- uses a static alternative. History, duplicates and reconnection baselines do not trigger
270
- automatic playback. Automatic animations require a visible conversation in a visible tab.
271
- With `backgroundSound` (default `true`), an effect nobody is watching — hidden tab, closed
272
- chat window or another conversation — still plays its sound, like new-message sounds; returning
273
- to that conversation within `maxAgeMs` replays it with its animation. Same-origin
274
- tab deduplication is best effort. Receiving clients use a cooldown and ignore stale events.
275
- The server should enforce its own authorization and rate limits; browser controls alone
276
- cannot prevent abusive clients.
277
-
278
- In the demo, **Nuevas funciones** lets you simulate effects from Ana and configure playback.
279
- The demo permits effects in groups and uses the same 5-second cooldown. None of these
280
- features requires a paid audio API. Sharing full songs and managing a production emoji
281
- upload service are outside this implementation.
282
-
283
- ## YouTube videos in messages
284
-
285
- Paste a YouTube video link into a message to display an inline player. Supported links
286
- include `watch?v=`, `youtu.be`, Shorts, Live and embed URLs, including start timestamps.
287
- The first supported video per message is embedded, with lazy loading and no autoplay.
288
- The original message is preserved and an **Abrir en YouTube** link is always available
289
- for videos that cannot play in an embedded player. No API key is required.
290
-
291
- Set `config.features.youtube` to `false` to disable previews. Playback requires internet
292
- access and a video that permits embedding. Applications with a Content Security Policy
293
- must allow `https://www.youtube-nocookie.com` in `frame-src`.
294
-
295
- ## Proposed audio samples (demo only)
296
-
297
- Run `npm run dev`, open **Nuevas funciones**, and scroll to **Audios de los efectos**.
298
- The demo offers WAV previews of a disco/funk pig beat (104 BPM, two bars), a new nudge,
299
- and a festive confetti cue, alongside the current synthesized sounds for comparison.
300
- All three approved effects now use MP3 recordings. The pig WAV remains an alternative
301
- proposal; the nudge and confetti WAVs are the original sources of the approved recordings.
302
-
303
- `predev` generates the WAV files automatically. Use `npm run audio:samples` to regenerate
304
- them separately. Generated files live in `demo/audio-samples/`, are ignored by Git and
305
- excluded from npm. Source generators are in `demo/audio-lab/`; see
306
- [docs/funciones/efectos.md](docs/funciones/efectos.md#audio) in the source repository for the arrangement and limitations.
307
-
308
- ### Fun chat defaults
309
-
310
- New incoming effects automatically animate in the visible conversation by default;
311
- manual replay is still available. The default cooldown is 5 seconds. Window focus is
312
- not required; hidden tabs only play the sound (see `backgroundSound`) and old messages do not replay effects. A new permitted
313
- effect can replace the animation currently playing. Confetti fills the panel with
314
- 220 colored particles and scales to its height. Reduced-motion preferences remain respected.
315
- Browsers can still block sound before the recipient interacts with the site; the visual
316
- effect does not depend on successful audio playback. Both participants must use an updated
317
- package, and explicitly saved `autoPlay: false` settings still disable automatic playback.
318
-
319
- ## Tanques LIVE 3D (preview)
320
-
321
- This fourth game is independent of turn-based Tanks, Connect 4 and Battleship. Enable it explicitly:
321
+ The values above are the defaults. Behavior:
322
+
323
+ - Received effects appear as cards with **Reproducir efecto** and autoplay in the visible conversation of a
324
+ visible tab. A new effect can replace the one playing. Escape or **×** closes it.
325
+ - With `backgroundSound`, an effect nobody is watching (hidden tab, closed window, another conversation) still
326
+ plays its sound; returning to that conversation within `maxAgeMs` replays it with the animation.
327
+ - History, duplicates and events older than `maxAgeMs` do not autoplay. Sends and autoplay respect
328
+ `cooldownMs`. Same-origin tab deduplication is best effort.
329
+ - The picker's **Sonido** and auto-play switches call `onPreferencesChange({ sound })` / `({ autoPlay })` so the
330
+ host can persist them. Browsers may block audio until the user interacts; visuals do not depend on it.
331
+ - Receiving clients enforce cooldown and age limits, but the server should enforce its own authorization and
332
+ rate limits.
333
+
334
+ ### Countdown messages
335
+
336
+ In the effects picker choose **Cuenta regresiva**, a duration (30 s, 1 min, 5 min, 10 min or a custom value in
337
+ seconds or minutes, up to 24 hours), the final effect and whether it plays sound. **Probar efecto** previews it
338
+ locally. The card shows a live clock, hides the chosen effect until zero (also in quotes and previews), animates
339
+ the last ten seconds and plays the effect at zero in the visible conversation. Expired cards offer manual replay.
340
+ Device clocks should be accurate.
341
+
342
+ Effects and countdowns are ordinary text messages (`[minicode-effect:v1]` / `[minicode-countdown:v1]` plus
343
+ JSON and a readable fallback line for older clients). No backend change is needed; the server must preserve the
344
+ text.
345
+
346
+ ## Games
347
+
348
+ All games are two-player, in private chats, started from the composer's games menu. Game events are chat
349
+ messages with the `LIRA_GAME_V1:` prefix; they never appear as bubbles or unread messages.
350
+
351
+ | Game | Flag (`features.games`) | Default | Rendering |
352
+ |---|---|---|---|
353
+ | Conecta 4 | `connect4` | `true` | DOM board |
354
+ | Batalla Naval 3D | `battleship` | `true` | `three` scene; 2D grid fallback. Fleets stay private until the final reveal (commit–reveal). |
355
+ | Tanques de guerra (turn-based) | `tanks` | `false` | `three` scene; Canvas 2D fallback. Deterministic simulation on both clients. |
356
+ | Tanques LIVE 3D (real time) | `tanksLive` | `false` | `three` scene; Canvas 2D fallback. WebRTC DataChannels. |
357
+
358
+ Optional Batalla Naval 3D preview (a local prototype opened from the games menu):
359
+
360
+ ```jsx
361
+ import { lazy } from 'react';
362
+ import { ChatWindow, loadBattleshipPreview } from '@minicode-dev/chat';
363
+
364
+ const BattleshipPreview = lazy(loadBattleshipPreview);
365
+ // <ChatWindow BattleshipPreviewComponent={BattleshipPreview} />
366
+ ```
367
+
368
+ ### Tanques LIVE 3D (preview)
322
369
 
323
370
  ```js
324
371
  const config = {
325
372
  features: { games: { tanksLive: true } },
326
- // Optional: supply runtime STUN/TURN settings from your application.
373
+ // Optional: STUN/TURN supplied by your application at runtime.
327
374
  webrtc: { iceServers, iceTransportPolicy: 'all' },
328
375
  };
329
376
  ```
330
377
 
331
- Omit `webrtc` to use the existing default STUN servers. Never ship permanent TURN credentials.
332
- Both players need a compatible package version and a browser with WebRTC. Invitations and
333
- acceptance use the chat transport; movement and combat use DataChannels. The inviting player
334
- resolves damage. Three.js loads dynamically, with a Canvas 2D fallback. Shared game mute and
335
- volume settings apply to the existing tank sounds.
336
-
337
- For a local two-player test, run `npm run dev -- --host` and open
338
- `/tanks-live-lab.html?user=ana` and `/tanks-live-lab.html?user=luis` before inviting.
339
- Use WASD/arrows, mouse and click/space on desktop, or the joystick, arena touch aiming and fire
340
- button on mobile. Minimize preserves the match; reload/reopening the chat is not a persisted
341
- match recovery flow. Wi-Fi versus cellular connectivity and runtime TURN still require real-device
342
- validation.
343
-
344
- ### Manual TURN recovery inside TANQUES LIVE
345
-
346
- Both players need a version with manual TURN support. No Django endpoint, consumer,
347
- WebSocket protocol change, Cloudflare SDK, or application-level screen is required.
348
- The existing `webrtc.iceServers` and `iceTransportPolicy` provider settings continue
349
- working. Manual settings belong only to the current match and are not persisted.
350
-
351
- 1. Invite the other player and accept the match normally. Local/direct connections
352
- keep working without TURN.
353
- 2. During negotiation (or even during a connected match), choose **Configurar
354
- conexión alternativa**. A slow or failed negotiation also shows a connection notice.
355
- 3. **One player** pastes the JSON containing temporary `iceServers` and selects
356
- **Aplicar y reconectar**. The other player receives and applies it automatically;
357
- they do not need to paste anything.
358
- 4. Wait for the sharing confirmation, then inspect the actual connection state.
359
- Applied credentials do not by themselves prove that TURN is reachable or in use.
360
-
361
- The modal accepts `{ "iceServers": [...] }` and a full Cloudflare envelope such as
362
- `{ "success": true, "result": { "iceServers": [...] } }`. It rejects invalid JSON,
363
- STUN-only configurations, invalid ICE URLs, missing TURN passwords, known expired
364
- credentials, and API-token fields. Limits: 6,000 characters/UTF-8 bytes, eight ICE
365
- servers and sixteen URLs. Only `stun:`, `stuns:`, `turn:` and `turns:` URLs are allowed.
366
-
367
- **Never paste the permanent Cloudflare TURN API token.** Generate temporary
368
- credentials externally in your terminal. For example, with your credentials already
369
- set in your terminal environment (not in React), this copies only the temporary
370
- response to the macOS clipboard:
378
+ - Simultaneous 1 vs 1 combat. Invitation and acceptance use the chat (`LIRA_GAME_V1:`); WebRTC signaling
379
+ (SDP/ICE) travels as `LIRA_WEBRTC_V1:` chat messages, at most 10,000 characters. Movement and combat use two
380
+ DataChannels; the inviting player resolves damage.
381
+ - **The chat backend (for example Django) is only the signaling relay.** It needs no WebRTC endpoint or
382
+ protocol change and never carries game traffic, which goes peer to peer or through TURN.
383
+ - Without `webrtc`, `DEFAULT_ICE_SERVERS` (public Google and Cloudflare STUN) is used. Never ship permanent TURN
384
+ credentials.
385
+ - Controls: WASD/arrows, mouse and click/Space on desktop; joystick, touch aiming and fire button on mobile.
386
+ Minimizing keeps the match; reloading does not recover it. Shares the tank sounds and game mute/volume.
387
+
388
+ #### Manual TURN recovery
389
+
390
+ When a direct connection fails, either player can open **Configurar conexión alternativa** (also suggested
391
+ after a slow or failed negotiation), paste temporary ICE credentials and choose **Aplicar y reconectar**. The
392
+ other player receives and applies them automatically. Settings last for the current match only.
393
+
394
+ - Accepted JSON: `{ "iceServers": [...] }` or a Cloudflare envelope `{ "success": true, "result": { "iceServers": [...] } }`,
395
+ optionally with `expiresAt` (ISO or Unix ms) or `expiresIn`/`ttl` (remaining seconds). The optional
396
+ **Validez restante en segundos** field is remaining validity, not the original TTL.
397
+ - Rejected: invalid JSON, STUN-only sets, invalid ICE URLs (only `stun:`, `stuns:`, `turn:`, `turns:`), TURN
398
+ without password, known-expired credentials and API-token fields. Limits: 6,000 characters/bytes, 8 servers,
399
+ 16 URLs.
400
+ - Credentials are encrypted with an ephemeral ECDH/AES-GCM exchange before entering the chat; no plaintext
401
+ password is sent. Requires HTTPS (or localhost) and Web Crypto; without it, sharing fails safely. This relies
402
+ on the authenticated private-room relay and does not protect against a malicious signaling server.
403
+ - **Never paste a permanent TURN API token.** Generate temporary credentials outside the app, for example
404
+ (credentials in your shell environment; copies the response to the macOS clipboard):
371
405
 
372
406
  ```sh
373
407
  curl --fail --silent --show-error \
@@ -377,59 +411,114 @@ curl --fail --silent --show-error \
377
411
  --data '{"ttl":3600}' | pbcopy
378
412
  ```
379
413
 
380
- The library never calls that API. The optional **Validez restante en segundos** field
381
- means remaining validity from now, not the original issuance TTL. Leave it blank
382
- when unknown; Cloudflare's opaque username is not treated as an expiry timestamp.
383
- You can also supply `expiresAt` (ISO text or Unix milliseconds) or `expiresIn`/`ttl`
384
- (remaining seconds) in the JSON. Known-expired settings are refused; expiry unknown
385
- cannot be detected reliably from every ICE authentication failure, so obtain and
386
- paste new credentials if the old ones stop working.
387
-
388
- Recovery uses the private chat signaling already used for SDP/ICE. A tab-bound,
389
- ephemeral ECDH/AES-GCM exchange encrypts the credentials before they enter the chat
390
- transport. The host coordinates preparation, confirmation and recovery so either
391
- player can initiate without racing two restart offers. A healthy direct connection
392
- with policy `all` is kept; new fallback settings do not trigger an unnecessary
393
- restart. Otherwise the link updates `setConfiguration()` and restarts ICE, or
394
- recreates only PeerConnections when necessary. Positions, health and match state
395
- remain in the existing simulation. Drafts clear on closing/applying the dialog;
396
- credentials and encryption keys clear when the match ends or is destroyed.
397
-
398
- The library excludes these internal frames from its visible client history and
399
- unread counts; any server that stores all chat frames may still retain encrypted
400
- signaling. No plaintext TURN password is sent in those frames. This relies on the
401
- existing authenticated private-room relay; it is not protection against an actively
402
- malicious signaling server. HTTPS (or localhost) and Web Crypto are required. Without
403
- Web Crypto, sharing fails safely instead of sending plaintext. Closing the dialog
404
- does not end the match; an already-started synchronization can still finish.
405
-
406
- **Diagnostics and real-network validation:**
407
-
408
- - Open **Diagnóstico de conexión** to see SDP/ICE/gathering states, DataChannel
409
- readiness, reconnects, connection time, RTT and sanitized ICE errors/counters.
410
- - `turnConfigured` only means a TURN URL is configured. `candidates.localRelay`
411
- means relay candidates were gathered. `selectedPair.relay: true` means the actual
412
- selected candidate pair uses TURN. No passwords, usernames, SDP or private candidate
413
- addresses are included in the panel.
414
- - To verify TURN, temporarily enable **Forzar relay para diagnóstico**. It is shared
415
- with the settings so both peers use `iceTransportPolicy: "relay"`. Confirm both
416
- DataChannels are open, `phase` is `connected`, the selected pair is relay, and
417
- movement counters increase on both devices. Disable the option for normal play.
418
- - Test from two devices on different networks (for example Wi-Fi and cellular),
419
- with current temporary credentials. Also test interruption/retry and renewing
420
- credentials from either player. No implementation can guarantee connectivity
421
- through every restrictive firewall; TURN/TLS over TCP may add latency/jitter.
422
- - The local two-player demo remains `/tanks-live-lab.html?user=ana` and
423
- `/tanks-live-lab.html?user=luis`. Automated tests use fake RTC/TURN to verify
424
- synchronization, retries, privacy and state preservation; they do not prove that
425
- a real TURN service is reachable across networks.
426
-
427
- For repeatable browser QA, `scripts/check-manual-turn-browser.mjs` uses Playwright
428
- as an optional development tool (not a library dependency). Run the demo, make a
429
- Playwright installation available, then run the script. `CHAT_DEMO_URL` overrides
430
- the default `http://127.0.0.1:5173`; `PLAYWRIGHT_MODULE` and
431
- `PLAYWRIGHT_EXECUTABLE_PATH` can select an existing tooling installation. It checks
432
- two real local browser peers, host/guest manual sharing with dummy credentials,
433
- invalid inputs, light/dark/mobile dialogs, cancellation and simulated blocked ICE.
434
- Its headless-only flags allow local candidates without mDNS resolution. The script
435
- never tests real TURN credentials or proves Wi-Fi/cellular connectivity.
414
+ - **Diagnóstico de conexión** shows SDP/ICE/DataChannel state, reconnects, RTT and sanitized errors, without
415
+ passwords, usernames, SDP or private addresses. `turnConfigured` only means a TURN URL is set;
416
+ `selectedPair.relay: true` means traffic actually uses TURN. **Forzar relay para diagnóstico** makes both
417
+ peers use `iceTransportPolicy: "relay"` to prove TURN works; turn it off for normal play.
418
+ - Real-network behavior (Wi-Fi vs cellular, restrictive firewalls) needs testing on real devices; automated
419
+ tests use fake RTC/TURN.
420
+
421
+ ### 3D and `three`
422
+
423
+ | Scene | Loaded | Without `three`/WebGL |
424
+ |---|---|---|
425
+ | Batalla Naval board and preview | When the board is shown / preview opened | 2D grid (preview needs WebGL) |
426
+ | Tanques de guerra | When the match is shown | Canvas 2D |
427
+ | Tanques LIVE | When the match starts | Canvas 2D |
428
+ | Zumbido, Confeti, Avalancha | When the effect plays | Canvas 2D (Zumbido) or CSS |
429
+
430
+ `three` never enters the main bundle; each scene is a separate chunk.
431
+
432
+ ## Wire protocol
433
+
434
+ Everything travels over the chat transport. Special payloads are ordinary `CHAT_MESSAGE` text with a prefix,
435
+ so the server only has to relay text unchanged, stamped with the sender's `user_id`.
436
+
437
+ | Prefix | Purpose | Shown in history |
438
+ |---|---|---|
439
+ | `LIRA_EDIT_V1:` / `LIRA_REACT_V1:` | Edit / reaction on an earlier message | No (applied to the target) |
440
+ | `sticker:` | Sticker attachment name | As a sticker |
441
+ | `LIRA_GAME_V1:` | Game events (private rooms only) | No (game card) |
442
+ | `LIRA_WEBRTC_V1:` | WebRTC signaling, including encrypted manual TURN | No, never counted as unread |
443
+ | `[minicode-effect:v1]` / `[minicode-countdown:v1]` | Effects / countdowns | As effect cards |
444
+
445
+ Requests carry a `request_id` and resolve on `CHAT_ACK` (or reject on a correlated `ERROR`). The frozen
446
+ protocol is covered by `test/contract/`.
447
+
448
+ ## Local development
449
+
450
+ ```bash
451
+ npm install
452
+ npm run dev # predev regenerates the demo WAV samples, then Vite opens the demo
453
+ ```
454
+
455
+ The demo (`demo/`, not published) runs the library source with simulated users and in-memory messages: automatic
456
+ replies, groups, emoji, custom emoji uploader, reactions, editing, attachments, voice notes, light/dark theme and
457
+ **Nuevas funciones** (simulate incoming effects, playback settings, audio lab). No login or backend is needed.
458
+
459
+ - **🪖 Tanques con Ana:** turn-based Tanques against a demo bot. Conecta 4 and Batalla Naval have no demo opponent.
460
+ - **Tanques LIVE:** `npm run dev -- --host`, then open `/tanks-live-lab.html?user=ana` and `?user=luis` before
461
+ inviting (a dev-only Vite relay plays the chat server).
462
+ - Labs: `/webrtc-lab.html`, `/tanks-lab.html`, `/avalanche-lab.html`.
463
+ - GIF/sticker search needs your own KLIPY key (`configureKlipy`); the demo does not configure one.
464
+ - Browser QA for manual TURN: `scripts/check-manual-turn-browser.mjs` (needs Playwright installed separately;
465
+ `CHAT_DEMO_URL`, `PLAYWRIGHT_MODULE`, `PLAYWRIGHT_EXECUTABLE_PATH`). It never uses real TURN credentials.
466
+ - Tests use Node's runner, for example `node --test test/effects/*.test.mjs test/unit/*.test.mjs`. See
467
+ `docs/desarrollo/demo-y-pruebas.md` for every suite.
468
+
469
+ ### Audio audition lab (demo only)
470
+
471
+ **Nuevas funciones → Audios de los efectos** plays procedurally generated WAVs next to the shipped MP3
472
+ (**Escuchar audio integrado**). `predev` runs `npm run audio:samples`, which writes them to `demo/audio-samples/`
473
+ (git-ignored, not published) from the generators in `demo/audio-lab/`:
474
+
475
+ - `pig-disco.wav` (~5 s, 104 BPM): an alternative proposal; the shipped pig is `cerdito_disco_original.mp3`.
476
+ - `nudge.wav` (0.9 s) and `confetti.wav` (2.7 s): sources of the earlier short cues only. The current ~7 s
477
+ `nudge.mp3` and `confetti.mp3` are separate recordings and do not come from these WAVs.
478
+ - Avalancha has no WAV; `avalanche.mp3` is its approved ~7 s recording.
479
+
480
+ The lab's own text still calls the nudge and confetti WAVs the sources of the current recordings; that is out of
481
+ date. Details: `docs/funciones/efectos.md` (section "Audio").
482
+
483
+ ### Build, pack and source mode
484
+
485
+ ```bash
486
+ npm run build # ESM library, lazy chunks and dist/styles.css
487
+ npm pack # builds again via prepack and creates minicode-dev-chat-<version>.tgz
488
+ ```
489
+
490
+ Install the tarball in a React app to test (`npm install /path/to/minicode-chat/minicode-dev-chat-<version>.tgz`)
491
+ and repeat after each change. The custom `@minicode-dev/source` export condition points at `src/`; tooling must
492
+ enable it explicitly (for example Vite `resolve.conditions`) and React and the other peers must resolve to one copy.
493
+
494
+ ## Publishing updates
495
+
496
+ Log in with `npm login`, then:
497
+
498
+ ```bash
499
+ npm run release # patch version, build, validate contents, publish
500
+ npm run release -- minor # minor version
501
+ npm run release -- major # major version
502
+ npm run release -- --dry-run # build and inspect; no version change or publication
503
+ ```
504
+
505
+ The script checks your npm session, builds, bumps the version without a Git commit or tag, and validates the exact
506
+ archive (only `dist/`, `assets/`, `index.d.ts`, `README.md`, `CHANGELOG.md` and `package.json`) before
507
+ publishing. Temporary archives and root `.tgz` files are removed even on failure; `dist/` is kept. If publication
508
+ fails after the bump, the new version stays in `package.json` and the lockfile. No Git push is performed. The
509
+ script is called `release` because npm reserves `publish` as a lifecycle hook.
510
+
511
+ ## Documentation
512
+
513
+ Internal docs (Spanish) live in the source repository and are not published to npm:
514
+
515
+ - [docs/README.md](docs/README.md): index.
516
+ - [docs/contexto-para-ia.md](docs/contexto-para-ia.md): rules, status, code map, protocols and pending work.
517
+ - Features: [docs/funciones/README.md](docs/funciones/README.md), [efectos.md](docs/funciones/efectos.md),
518
+ [emojis-personalizados.md](docs/funciones/emojis-personalizados.md), [youtube.md](docs/funciones/youtube.md).
519
+ - Games: [docs/juegos/README.md](docs/juegos/README.md),
520
+ [Tanques LIVE progress](docs/juegos/tanques-live/progreso-webrtc.md).
521
+ - Development: [demo-y-pruebas.md](docs/desarrollo/demo-y-pruebas.md),
522
+ [integracion-consumidor.md](docs/desarrollo/integracion-consumidor.md),
523
+ [publicacion.md](docs/desarrollo/publicacion.md).
524
+ - Changes per version: [CHANGELOG.md](CHANGELOG.md).