dignity.js 0.4.0 → 0.5.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.
Files changed (38) hide show
  1. package/README.md +83 -2
  2. package/dist/dignity.cjs.js +542 -21
  3. package/dist/dignity.cjs.js.map +4 -4
  4. package/dist/dignity.esm.js +542 -21
  5. package/dist/dignity.esm.js.map +3 -3
  6. package/dist/dignity.min.js +18 -18
  7. package/docs/assets/dignity.esm.js +11205 -0
  8. package/docs/assets/favicon.svg +8 -0
  9. package/docs/chess/assets/chess-app.js +58022 -0
  10. package/docs/chess/assets/chess-app.js.map +7 -0
  11. package/docs/chess/assets/chess.css +584 -0
  12. package/docs/chess/favicon.ico +0 -0
  13. package/docs/chess/index.html +16 -0
  14. package/docs/chess/src/App.jsx +128 -0
  15. package/docs/chess/src/components/Board3D.jsx +364 -0
  16. package/docs/chess/src/components/GameView.jsx +847 -0
  17. package/docs/chess/src/components/JoinGate.jsx +68 -0
  18. package/docs/chess/src/components/LinkPanel.jsx +132 -0
  19. package/docs/chess/src/components/Lobby.jsx +154 -0
  20. package/docs/chess/src/components/MovePanel.jsx +123 -0
  21. package/docs/chess/src/lib/audio.js +50 -0
  22. package/docs/chess/src/lib/dignitySetup.js +42 -0
  23. package/docs/chess/src/lib/links.js +124 -0
  24. package/docs/chess/src/lib/localGames.js +160 -0
  25. package/docs/chess/src/lib/p2pDebug.js +192 -0
  26. package/docs/chess/src/main.jsx +5 -0
  27. package/docs/favicon.ico +0 -0
  28. package/docs/index.html +7 -3
  29. package/docs/openapi-like.json +35 -6
  30. package/examples/decentralized-chess-lite.js +52 -30
  31. package/package.json +12 -4
  32. package/src/core/dignity-p2p.js +388 -16
  33. package/src/index.js +6 -0
  34. package/src/network/peerjs-network.js +234 -0
  35. package/src/persistence/indexeddb-persistence.js +2 -0
  36. package/src/react/index.js +143 -1
  37. package/src/signaling/parse-peerjs-url.js +24 -0
  38. package/src/signaling/peerjs-signaling-provider.js +2 -8
@@ -0,0 +1,584 @@
1
+ :root {
2
+ --bg: #0f0b08;
3
+ --panel: #1b1410;
4
+ --panel-border: #3d2b1f;
5
+ --text: #f6efe6;
6
+ --muted: #b9a995;
7
+ --accent: #5b7fff;
8
+ --accent-soft: rgba(91, 127, 255, 0.16);
9
+ --wood: #c9a66b;
10
+ --success: #7ee787;
11
+ --danger: #ff7b72;
12
+ --radius: 12px;
13
+ --font: "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
14
+ }
15
+
16
+ * {
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ body {
21
+ margin: 0;
22
+ min-height: 100vh;
23
+ color: var(--text);
24
+ background:
25
+ radial-gradient(circle at top, rgba(201, 166, 107, 0.12), transparent 35%),
26
+ linear-gradient(180deg, #120d0a, #090605);
27
+ font-family: var(--font);
28
+ font-size: 18px;
29
+ line-height: 1.5;
30
+ }
31
+
32
+ button,
33
+ input,
34
+ textarea {
35
+ font: inherit;
36
+ }
37
+
38
+ a {
39
+ color: var(--accent);
40
+ text-decoration: none;
41
+ }
42
+
43
+ .app-shell {
44
+ min-height: 100vh;
45
+ height: 100dvh;
46
+ display: flex;
47
+ flex-direction: column;
48
+ overflow: hidden;
49
+ }
50
+
51
+ .app-shell--game .topbar,
52
+ .app-shell--game .footer {
53
+ padding-top: 8px;
54
+ padding-bottom: 8px;
55
+ }
56
+
57
+ .app-shell--game .footer {
58
+ font-size: 0.82rem;
59
+ }
60
+
61
+ .topbar,
62
+ .game-header,
63
+ .footer {
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: space-between;
67
+ gap: 16px;
68
+ padding: 14px 20px;
69
+ flex-shrink: 0;
70
+ }
71
+
72
+ .game-header {
73
+ padding: 10px 16px;
74
+ }
75
+
76
+ .topbar,
77
+ .game-header {
78
+ border-bottom: 1px solid var(--panel-border);
79
+ background: rgba(15, 11, 8, 0.88);
80
+ backdrop-filter: blur(8px);
81
+ }
82
+
83
+ .topbar label {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 10px;
87
+ color: var(--muted);
88
+ }
89
+
90
+ .topbar input,
91
+ .link-row input,
92
+ .lobby__join textarea {
93
+ width: 100%;
94
+ border: 1px solid var(--panel-border);
95
+ border-radius: 8px;
96
+ background: #120d0a;
97
+ color: var(--text);
98
+ padding: 10px 12px;
99
+ }
100
+
101
+ button {
102
+ border: none;
103
+ border-radius: 999px;
104
+ padding: 10px 16px;
105
+ cursor: pointer;
106
+ background: var(--accent);
107
+ color: white;
108
+ }
109
+
110
+ button.secondary,
111
+ button.ghost {
112
+ background: transparent;
113
+ border: 1px solid var(--panel-border);
114
+ color: var(--text);
115
+ }
116
+
117
+ button.ghost {
118
+ border-radius: 8px;
119
+ }
120
+
121
+ .lobby {
122
+ display: grid;
123
+ grid-template-columns: 1.2fr 1fr;
124
+ gap: 24px;
125
+ padding: 32px 24px 24px;
126
+ max-width: 1200px;
127
+ margin: 0 auto;
128
+ }
129
+
130
+ .lobby-layout {
131
+ flex: 1;
132
+ }
133
+
134
+ .lobby__history {
135
+ max-width: 1200px;
136
+ margin: 0 auto;
137
+ padding: 0 24px 48px;
138
+ }
139
+
140
+ .lobby__history-head {
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: space-between;
144
+ gap: 16px;
145
+ margin-bottom: 16px;
146
+ }
147
+
148
+ .lobby__history-head h2 {
149
+ margin: 0;
150
+ font-size: 1.35rem;
151
+ }
152
+
153
+ .lobby__history-grid {
154
+ display: grid;
155
+ grid-template-columns: 1fr 1fr;
156
+ gap: 20px;
157
+ }
158
+
159
+ .lobby__games h2 {
160
+ margin: 0 0 12px;
161
+ font-size: 1.1rem;
162
+ }
163
+
164
+ .game-list {
165
+ list-style: none;
166
+ margin: 0;
167
+ padding: 0;
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 10px;
171
+ }
172
+
173
+ .game-list__item {
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: space-between;
177
+ gap: 12px;
178
+ padding: 12px 0;
179
+ border-top: 1px solid var(--panel-border);
180
+ }
181
+
182
+ .game-list__item:first-child {
183
+ border-top: none;
184
+ padding-top: 0;
185
+ }
186
+
187
+ .game-list__meta {
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: 4px;
191
+ min-width: 0;
192
+ }
193
+
194
+ .game-list__meta strong {
195
+ font-size: 0.95rem;
196
+ word-break: break-all;
197
+ }
198
+
199
+ .lobby__hero,
200
+ .lobby__join,
201
+ .panel,
202
+ .link-panel {
203
+ background: rgba(27, 20, 16, 0.92);
204
+ border: 1px solid var(--panel-border);
205
+ border-radius: var(--radius);
206
+ padding: 24px;
207
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
208
+ }
209
+
210
+ .eyebrow {
211
+ text-transform: uppercase;
212
+ letter-spacing: 0.08em;
213
+ color: var(--wood);
214
+ font-size: 0.78rem;
215
+ font-weight: 700;
216
+ }
217
+
218
+ .lobby h1,
219
+ .game-header h2 {
220
+ margin: 0 0 8px;
221
+ font-size: 2.2rem;
222
+ }
223
+
224
+ .game-header h2 {
225
+ font-size: 1.45rem;
226
+ margin-bottom: 4px;
227
+ }
228
+
229
+ .game-header .status-line {
230
+ margin: 0;
231
+ font-size: 0.88rem;
232
+ }
233
+
234
+ .game-layout {
235
+ flex: 1;
236
+ min-height: 0;
237
+ display: flex;
238
+ flex-direction: column;
239
+ padding: 12px 16px;
240
+ overflow: hidden;
241
+ }
242
+
243
+ .game-grid {
244
+ flex: 1;
245
+ min-height: 0;
246
+ display: grid;
247
+ grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.8fr);
248
+ grid-template-rows: minmax(0, 1fr);
249
+ gap: 16px;
250
+ align-items: stretch;
251
+ }
252
+
253
+ .board-3d {
254
+ height: 100%;
255
+ min-height: 0;
256
+ border-radius: var(--radius);
257
+ overflow: hidden;
258
+ border: 1px solid var(--panel-border);
259
+ background: #120d0a;
260
+ position: relative;
261
+ }
262
+
263
+ .board-3d--interactive {
264
+ cursor: pointer;
265
+ }
266
+
267
+ .board-3d__hint {
268
+ position: absolute;
269
+ left: 12px;
270
+ bottom: 12px;
271
+ margin: 0;
272
+ padding: 6px 10px;
273
+ border-radius: 6px;
274
+ background: rgba(0, 0, 0, 0.55);
275
+ color: var(--muted);
276
+ font-size: 0.82rem;
277
+ pointer-events: none;
278
+ z-index: 2;
279
+ }
280
+
281
+ .move-panel__status {
282
+ margin: 0 0 12px;
283
+ color: var(--muted);
284
+ font-size: 0.95rem;
285
+ }
286
+
287
+ .move-panel__selected {
288
+ margin: 0 0 12px;
289
+ }
290
+
291
+ .mini-board {
292
+ display: inline-block;
293
+ border: 2px solid var(--panel-border);
294
+ border-radius: 6px;
295
+ overflow: hidden;
296
+ }
297
+
298
+ .mini-board__row {
299
+ display: flex;
300
+ align-items: center;
301
+ }
302
+
303
+ .mini-board__rank {
304
+ width: 18px;
305
+ text-align: center;
306
+ font-size: 0.72rem;
307
+ color: var(--muted);
308
+ background: rgba(0, 0, 0, 0.25);
309
+ }
310
+
311
+ .mini-board__files {
312
+ display: grid;
313
+ grid-template-columns: 18px repeat(8, 36px);
314
+ font-size: 0.72rem;
315
+ color: var(--muted);
316
+ text-align: center;
317
+ background: rgba(0, 0, 0, 0.25);
318
+ }
319
+
320
+ .mini-board__file {
321
+ line-height: 18px;
322
+ }
323
+
324
+ .mini-board__sq {
325
+ width: 36px;
326
+ height: 36px;
327
+ padding: 0;
328
+ border: none;
329
+ border-radius: 0;
330
+ font-size: 1.35rem;
331
+ line-height: 1;
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: center;
335
+ cursor: pointer;
336
+ color: inherit;
337
+ }
338
+
339
+ .mini-board__sq.light {
340
+ background: #c9a66b;
341
+ }
342
+
343
+ .mini-board__sq.dark {
344
+ background: #6b4423;
345
+ }
346
+
347
+ .mini-board__piece {
348
+ pointer-events: none;
349
+ line-height: 1;
350
+ }
351
+
352
+ .mini-board__piece--w {
353
+ color: #f6efe6;
354
+ text-shadow: 0 0 2px #1a1410, 0 1px 3px #1a1410;
355
+ }
356
+
357
+ .mini-board__piece--b {
358
+ color: #1a1410;
359
+ text-shadow: 0 0 1px rgba(246, 239, 230, 0.55);
360
+ }
361
+
362
+ .mini-board__sq.selected {
363
+ box-shadow: inset 0 0 0 3px var(--accent);
364
+ }
365
+
366
+ .mini-board__sq.target {
367
+ box-shadow: inset 0 0 0 3px var(--success);
368
+ }
369
+
370
+ .mini-board__sq:disabled {
371
+ cursor: default;
372
+ opacity: 0.85;
373
+ }
374
+
375
+ .move-targets {
376
+ margin-top: 14px;
377
+ }
378
+
379
+ .move-targets__list {
380
+ display: flex;
381
+ flex-wrap: wrap;
382
+ gap: 8px;
383
+ }
384
+
385
+ .move-targets__list button {
386
+ font-size: 0.85rem;
387
+ padding: 6px 12px;
388
+ }
389
+
390
+ .side-panel {
391
+ display: flex;
392
+ flex-direction: column;
393
+ gap: 16px;
394
+ min-height: 0;
395
+ overflow-y: auto;
396
+ padding-right: 2px;
397
+ }
398
+
399
+ .panel h3,
400
+ .panel h4,
401
+ .link-panel h3 {
402
+ margin: 0 0 10px;
403
+ }
404
+
405
+ .link-panel__head {
406
+ display: flex;
407
+ align-items: center;
408
+ justify-content: space-between;
409
+ gap: 12px;
410
+ margin-bottom: 10px;
411
+ }
412
+
413
+ .link-panel__head h3 {
414
+ margin: 0;
415
+ }
416
+
417
+ .link-panel__toggle {
418
+ flex-shrink: 0;
419
+ padding: 6px 12px;
420
+ font-size: 0.85rem;
421
+ }
422
+
423
+ .link-panel__summary {
424
+ margin: 0;
425
+ color: var(--muted);
426
+ font-size: 0.88rem;
427
+ }
428
+
429
+ .link-panel--collapsed .link-panel__head {
430
+ margin-bottom: 6px;
431
+ }
432
+
433
+ .link-panel--collapsed .link-panel__body {
434
+ display: none;
435
+ }
436
+
437
+ .link-panel--prominent.link-panel--collapsed {
438
+ padding: 12px 16px;
439
+ }
440
+
441
+ .panel ul,
442
+ .panel ol,
443
+ .moves ol {
444
+ margin: 0;
445
+ padding-left: 1.2rem;
446
+ }
447
+
448
+ .muted {
449
+ color: var(--muted);
450
+ }
451
+
452
+ .badge,
453
+ .notice,
454
+ .status-line {
455
+ color: var(--muted);
456
+ }
457
+
458
+ .notice {
459
+ flex-shrink: 0;
460
+ margin: 0 0 10px;
461
+ padding: 8px 12px;
462
+ border-radius: 8px;
463
+ background: var(--accent-soft);
464
+ color: #dbe4ff;
465
+ font-size: 0.92rem;
466
+ }
467
+
468
+ .link-panel__hint {
469
+ color: var(--muted);
470
+ margin-top: 0;
471
+ }
472
+
473
+ .link-row {
474
+ display: grid;
475
+ grid-template-columns: 120px 1fr auto auto;
476
+ gap: 10px;
477
+ align-items: center;
478
+ margin-bottom: 12px;
479
+ }
480
+
481
+ .link-panel--prominent {
482
+ flex-shrink: 0;
483
+ margin-bottom: 10px;
484
+ border-color: var(--wood);
485
+ background: rgba(201, 166, 107, 0.08);
486
+ }
487
+
488
+ .link-panel--prominent h3 {
489
+ color: var(--wood);
490
+ }
491
+
492
+ .link-row input:focus {
493
+ outline: 2px solid var(--accent);
494
+ outline-offset: 1px;
495
+ }
496
+
497
+ .link-row label {
498
+ color: var(--muted);
499
+ font-size: 0.92rem;
500
+ }
501
+
502
+ .lobby__nickname {
503
+ display: flex;
504
+ flex-direction: column;
505
+ gap: 8px;
506
+ margin: 20px 0;
507
+ color: var(--muted);
508
+ }
509
+
510
+ .join-gate {
511
+ display: flex;
512
+ justify-content: center;
513
+ padding: 40px 20px 64px;
514
+ }
515
+
516
+ .join-gate__card {
517
+ width: min(100%, 520px);
518
+ }
519
+
520
+ .join-gate__field {
521
+ display: flex;
522
+ flex-direction: column;
523
+ gap: 8px;
524
+ margin: 20px 0;
525
+ color: var(--muted);
526
+ }
527
+
528
+ .join-gate__field input {
529
+ border: 1px solid var(--panel-border);
530
+ border-radius: 8px;
531
+ background: #120d0a;
532
+ color: var(--text);
533
+ padding: 12px 14px;
534
+ font-size: 1.05rem;
535
+ }
536
+
537
+ .join-gate__actions {
538
+ display: flex;
539
+ gap: 12px;
540
+ justify-content: flex-end;
541
+ }
542
+
543
+ .topbar__hint,
544
+ .topbar__player {
545
+ color: var(--muted);
546
+ font-size: 0.95rem;
547
+ }
548
+
549
+ .error-panel {
550
+ max-width: 640px;
551
+ margin: 40px auto;
552
+ }
553
+
554
+ .footer {
555
+ color: var(--muted);
556
+ font-size: 0.92rem;
557
+ border-top: 1px solid var(--panel-border);
558
+ }
559
+
560
+ @media (max-width: 960px) {
561
+ .lobby,
562
+ .lobby__history-grid,
563
+ .game-grid,
564
+ .link-row {
565
+ grid-template-columns: 1fr;
566
+ }
567
+
568
+ .app-shell--game {
569
+ overflow-y: auto;
570
+ }
571
+
572
+ .game-layout {
573
+ overflow: visible;
574
+ min-height: auto;
575
+ }
576
+
577
+ .game-grid {
578
+ grid-template-rows: minmax(280px, 42dvh) auto;
579
+ }
580
+
581
+ .board-3d {
582
+ min-height: 280px;
583
+ }
584
+ }
Binary file
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Decentralized 3D chess demo powered by dignity.js, PeerJS, and React." />
7
+ <title>3D Chess · dignity.js demo</title>
8
+ <link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
9
+ <link rel="icon" href="./favicon.ico" sizes="32x32" />
10
+ <link rel="stylesheet" href="./assets/chess.css" />
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="./assets/chess-app.js"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,128 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import Lobby from './components/Lobby.jsx';
3
+ import JoinGate from './components/JoinGate.jsx';
4
+ import GameView from './components/GameView.jsx';
5
+ import { nodeIdForRole, parseRoute, randomToken, scopeForGame } from './lib/links.js';
6
+ import { saveLocalGameSession } from './lib/localGames.js';
7
+
8
+ const STORAGE_KEY = 'dignity-chess-name';
9
+
10
+ export default function App() {
11
+ const [route, setRoute] = useState(() => parseRoute());
12
+ const [draftNickname, setDraftNickname] = useState(
13
+ () => localStorage.getItem(STORAGE_KEY) || 'Player'
14
+ );
15
+ const [sessionNickname, setSessionNickname] = useState(null);
16
+
17
+ useEffect(() => {
18
+ const handleHashChange = () => setRoute(parseRoute());
19
+ window.addEventListener('hashchange', handleHashChange);
20
+ return () => window.removeEventListener('hashchange', handleHashChange);
21
+ }, []);
22
+
23
+ const activeSession = route.gameId && route.roomKey;
24
+
25
+ const nodeId = useMemo(() => {
26
+ if (!activeSession || !sessionNickname) {
27
+ return null;
28
+ }
29
+ return nodeIdForRole(route.role, route.gameId);
30
+ }, [activeSession, sessionNickname, route.role, route.gameId]);
31
+
32
+ function persistNickname(name) {
33
+ const trimmed = name.trim() || 'Player';
34
+ localStorage.setItem(STORAGE_KEY, trimmed);
35
+ setDraftNickname(trimmed);
36
+ setSessionNickname(trimmed);
37
+ return trimmed;
38
+ }
39
+
40
+ function startGame(gameId) {
41
+ persistNickname(draftNickname);
42
+ const roomKey = randomToken(16);
43
+ const joinToken = randomToken();
44
+ const watchToken = randomToken();
45
+ const resumeToken = randomToken();
46
+ window.location.hash = [
47
+ `game=${encodeURIComponent(gameId)}`,
48
+ `room=${encodeURIComponent(roomKey)}`,
49
+ 'role=host',
50
+ `join=${encodeURIComponent(joinToken)}`,
51
+ `watch=${encodeURIComponent(watchToken)}`,
52
+ `resume=${encodeURIComponent(resumeToken)}`
53
+ ].join('&');
54
+ setRoute(parseRoute());
55
+ saveLocalGameSession({
56
+ gameId,
57
+ roomKey,
58
+ role: 'host',
59
+ joinToken,
60
+ watchToken,
61
+ resumeToken,
62
+ nickname: draftNickname.trim() || 'Player',
63
+ status: 'waiting',
64
+ moveCount: 0,
65
+ updatedAt: Date.now()
66
+ });
67
+ }
68
+
69
+ function openSavedGame(hash) {
70
+ window.location.hash = hash;
71
+ setRoute(parseRoute());
72
+ setSessionNickname(null);
73
+ }
74
+
75
+ function backToLobby() {
76
+ window.location.hash = '';
77
+ setRoute(parseRoute());
78
+ setSessionNickname(null);
79
+ }
80
+
81
+ function openPastedLink(raw) {
82
+ const hashIndex = raw.indexOf('#');
83
+ window.location.hash = hashIndex >= 0 ? raw.slice(hashIndex + 1) : raw;
84
+ setRoute(parseRoute());
85
+ setSessionNickname(null);
86
+ }
87
+
88
+ return (
89
+ <div className={`app-shell${activeSession && sessionNickname ? ' app-shell--game' : ''}`}>
90
+ <header className="topbar">
91
+ <a href="../index.html">dignity.js docs</a>
92
+ {!activeSession ? (
93
+ <span className="topbar__hint">Set your nickname in the lobby before starting or joining.</span>
94
+ ) : sessionNickname ? (
95
+ <span className="topbar__player">Playing as <strong>{sessionNickname}</strong></span>
96
+ ) : null}
97
+ </header>
98
+
99
+ {!activeSession ? (
100
+ <Lobby
101
+ nickname={draftNickname}
102
+ onNicknameChange={setDraftNickname}
103
+ onCreate={startGame}
104
+ onJoinPaste={openPastedLink}
105
+ onOpenGame={openSavedGame}
106
+ />
107
+ ) : !sessionNickname ? (
108
+ <JoinGate
109
+ route={route}
110
+ defaultNickname={draftNickname}
111
+ onConfirm={persistNickname}
112
+ onBack={backToLobby}
113
+ />
114
+ ) : (
115
+ <GameView
116
+ route={route}
117
+ nodeId={nodeId}
118
+ nickname={sessionNickname}
119
+ onBack={backToLobby}
120
+ />
121
+ )}
122
+
123
+ <footer className="footer">
124
+ Scope preview: {route.gameId ? scopeForGame(route.gameId) : '—'} · Cloudflare PeerJS · IndexedDB persistence
125
+ </footer>
126
+ </div>
127
+ );
128
+ }