dignity.js 0.3.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 (43) hide show
  1. package/README.md +142 -4
  2. package/dist/dignity.cjs.js +768 -20
  3. package/dist/dignity.cjs.js.map +4 -4
  4. package/dist/dignity.esm.js +768 -20
  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/docs.js +47 -0
  9. package/docs/assets/favicon.svg +8 -0
  10. package/docs/assets/highlight/github-dark.min.css +10 -0
  11. package/docs/assets/highlight/github.min.css +10 -0
  12. package/docs/assets/highlight/highlight.min.js +1244 -0
  13. package/docs/assets/styles.css +449 -38
  14. package/docs/chess/assets/chess-app.js +58022 -0
  15. package/docs/chess/assets/chess-app.js.map +7 -0
  16. package/docs/chess/assets/chess.css +584 -0
  17. package/docs/chess/favicon.ico +0 -0
  18. package/docs/chess/index.html +16 -0
  19. package/docs/chess/src/App.jsx +128 -0
  20. package/docs/chess/src/components/Board3D.jsx +364 -0
  21. package/docs/chess/src/components/GameView.jsx +847 -0
  22. package/docs/chess/src/components/JoinGate.jsx +68 -0
  23. package/docs/chess/src/components/LinkPanel.jsx +132 -0
  24. package/docs/chess/src/components/Lobby.jsx +154 -0
  25. package/docs/chess/src/components/MovePanel.jsx +123 -0
  26. package/docs/chess/src/lib/audio.js +50 -0
  27. package/docs/chess/src/lib/dignitySetup.js +42 -0
  28. package/docs/chess/src/lib/links.js +124 -0
  29. package/docs/chess/src/lib/localGames.js +160 -0
  30. package/docs/chess/src/lib/p2pDebug.js +192 -0
  31. package/docs/chess/src/main.jsx +5 -0
  32. package/docs/favicon.ico +0 -0
  33. package/docs/index.html +605 -81
  34. package/docs/openapi-like.json +74 -7
  35. package/examples/decentralized-chess-lite.js +52 -30
  36. package/package.json +30 -4
  37. package/src/core/dignity-p2p.js +466 -15
  38. package/src/index.js +8 -0
  39. package/src/network/peerjs-network.js +234 -0
  40. package/src/persistence/indexeddb-persistence.js +184 -0
  41. package/src/react/index.js +256 -0
  42. package/src/signaling/parse-peerjs-url.js +24 -0
  43. 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
+ }