@chatwarden/player-react 0.0.0-stage → 0.1.3

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 (84) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +401 -2
  3. package/dist/src/Dialog.d.ts +13 -0
  4. package/dist/src/Dialog.d.ts.map +1 -0
  5. package/dist/src/Dialog.js +83 -0
  6. package/dist/src/Dialog.js.map +1 -0
  7. package/dist/src/MyRecord.d.ts +12 -0
  8. package/dist/src/MyRecord.d.ts.map +1 -0
  9. package/dist/src/MyRecord.js +17 -0
  10. package/dist/src/MyRecord.js.map +1 -0
  11. package/dist/src/NoticeQueue.d.ts +26 -0
  12. package/dist/src/NoticeQueue.d.ts.map +1 -0
  13. package/dist/src/NoticeQueue.js +90 -0
  14. package/dist/src/NoticeQueue.js.map +1 -0
  15. package/dist/src/ReportDialog.d.ts +64 -0
  16. package/dist/src/ReportDialog.d.ts.map +1 -0
  17. package/dist/src/ReportDialog.js +146 -0
  18. package/dist/src/ReportDialog.js.map +1 -0
  19. package/dist/src/RestrictedChatLine.d.ts +12 -0
  20. package/dist/src/RestrictedChatLine.d.ts.map +1 -0
  21. package/dist/src/RestrictedChatLine.js +19 -0
  22. package/dist/src/RestrictedChatLine.js.map +1 -0
  23. package/dist/src/RulesPage.d.ts +10 -0
  24. package/dist/src/RulesPage.d.ts.map +1 -0
  25. package/dist/src/RulesPage.js +17 -0
  26. package/dist/src/RulesPage.js.map +1 -0
  27. package/dist/src/client.d.ts +51 -0
  28. package/dist/src/client.d.ts.map +1 -0
  29. package/dist/src/client.js +98 -0
  30. package/dist/src/client.js.map +1 -0
  31. package/dist/src/constants.d.ts +5 -0
  32. package/dist/src/constants.d.ts.map +1 -0
  33. package/dist/src/constants.js +5 -0
  34. package/dist/src/constants.js.map +1 -0
  35. package/dist/src/format.d.ts +14 -0
  36. package/dist/src/format.d.ts.map +1 -0
  37. package/dist/src/format.js +55 -0
  38. package/dist/src/format.js.map +1 -0
  39. package/dist/src/hooks.d.ts +15 -0
  40. package/dist/src/hooks.d.ts.map +1 -0
  41. package/dist/src/hooks.js +29 -0
  42. package/dist/src/hooks.js.map +1 -0
  43. package/dist/src/i18n.d.ts +153 -0
  44. package/dist/src/i18n.d.ts.map +1 -0
  45. package/dist/src/i18n.js +191 -0
  46. package/dist/src/i18n.js.map +1 -0
  47. package/dist/src/index.d.ts +21 -0
  48. package/dist/src/index.d.ts.map +1 -0
  49. package/dist/src/index.js +11 -0
  50. package/dist/src/index.js.map +1 -0
  51. package/dist/src/reporting.d.ts +20 -0
  52. package/dist/src/reporting.d.ts.map +1 -0
  53. package/dist/src/reporting.js +45 -0
  54. package/dist/src/reporting.js.map +1 -0
  55. package/dist/src/safeHref.d.ts +5 -0
  56. package/dist/src/safeHref.d.ts.map +1 -0
  57. package/dist/src/safeHref.js +41 -0
  58. package/dist/src/safeHref.js.map +1 -0
  59. package/dist/src/transport.d.ts +69 -0
  60. package/dist/src/transport.d.ts.map +1 -0
  61. package/dist/src/transport.js +74 -0
  62. package/dist/src/transport.js.map +1 -0
  63. package/dist/src/ui.d.ts +91 -0
  64. package/dist/src/ui.d.ts.map +1 -0
  65. package/dist/src/ui.js +116 -0
  66. package/dist/src/ui.js.map +1 -0
  67. package/package.json +73 -4
  68. package/src/Dialog.tsx +107 -0
  69. package/src/MyRecord.tsx +60 -0
  70. package/src/NoticeQueue.tsx +203 -0
  71. package/src/ReportDialog.tsx +319 -0
  72. package/src/RestrictedChatLine.tsx +42 -0
  73. package/src/RulesPage.tsx +49 -0
  74. package/src/client.ts +132 -0
  75. package/src/constants.ts +5 -0
  76. package/src/format.ts +53 -0
  77. package/src/hooks.ts +41 -0
  78. package/src/i18n.ts +211 -0
  79. package/src/index.ts +32 -0
  80. package/src/reporting.ts +51 -0
  81. package/src/safeHref.ts +36 -0
  82. package/src/styles.css +500 -0
  83. package/src/transport.ts +134 -0
  84. package/src/ui.tsx +199 -0
package/src/styles.css ADDED
@@ -0,0 +1,500 @@
1
+ /*
2
+ * @chatwarden/player-react
3
+ *
4
+ * One small stylesheet. Every colour, size, radius and space is a --cw-* custom property with a light and a dark default
5
+ * (light-dark()); every rule is scoped under .cw-player. Nothing is injected at run time, so a strict Content-Security-Policy
6
+ * needs nothing more than permission to load this file.
7
+ *
8
+ * Theming: set any --cw-* token on any element above the components (body, a wrapper), on .cw-player, or on the className you
9
+ * pass. The stylesheet never declares a public --cw-* name itself: the defaults sit under private --cwd-* names (declared with
10
+ * :where(), so any rule of yours wins whatever its order) and every rule reads var(--cw-x, var(--cwd-x)): your value if there is
11
+ * one anywhere up the chain, the default if not. A custom property declared on an element beats the value it inherits, so
12
+ * declaring --cw-* on .cw-player here would have hidden whatever the page sets above it. A theme is chosen by the `theme` prop
13
+ * (data-cw-theme="light|dark" on .cw-player), by the same attribute on any ancestor, or, by default, by the player's
14
+ * prefers-color-scheme. --cw-font is the one token with no default: it is read as var(--cw-font, inherit), so the font is the
15
+ * game's own unless you set it.
16
+ *
17
+ * It is neutral on purpose: it sits inside a game, so it takes the game's font (inherit) and uses a quiet grey and white base.
18
+ * To make it look like your game, set three things: --cw-surface (the base), --cw-accent (your next action) and --cw-radius.
19
+ * Every other token has a default that works on them.
20
+ *
21
+ * Colour only where it means something:
22
+ * accent the one next action, a link, a choice you made, and the focus ring
23
+ * red a failure, and the red side of a referee card
24
+ * yellow the yellow side of a referee card
25
+ * The referee card is the one signature detail, the same as in the moderator hub: yellow for a warning, half yellow and half red
26
+ * for a restriction, red for a ban, a dashed outline for a note. A sanction looks the same to staff and to the player.
27
+ */
28
+
29
+ /* ---- Tokens ------------------------------------------------------------------------------------------------------ */
30
+ :where(.cw-player) {
31
+ /* The defaults, under private names: --cwd-x is what --cw-x falls back to (see the top of this file). */
32
+ color-scheme: light dark;
33
+
34
+ /* Neutral base: the panel, the sunken well, the lines, and the text */
35
+ --cwd-surface: light-dark(#ffffff, #151c28);
36
+ --cwd-surface-sunken: light-dark(#eef0f4, #111722);
37
+ --cwd-border: light-dark(#e0e3ea, #242d3c);
38
+ --cwd-border-strong: light-dark(#7a8397, #6b7893);
39
+ --cwd-ink: light-dark(#151b27, #e8ebf1);
40
+ --cwd-ink-muted: light-dark(#4f5a6e, #9ba4b6);
41
+
42
+ /* The one accent: your next action, a link, a choice, the focus ring */
43
+ --cwd-accent: light-dark(#2950d3, #7d9bff);
44
+ --cwd-accent-ink: light-dark(#ffffff, #0e131c);
45
+ --cwd-accent-soft: light-dark(#e7ecfb, #1d2847);
46
+
47
+ /* Red: a failure. Yellow: the yellow of a referee card. Both are also the card. */
48
+ --cwd-red: light-dark(#c42f3b, #ff6b74);
49
+ --cwd-red-soft: light-dark(#fbe4e6, #3a1a1f);
50
+ --cwd-yellow: light-dark(#e5ad00, #f2c84b);
51
+
52
+ /* The ink every shadow and backdrop is made from */
53
+ --cwd-shadow-ink: light-dark(#151b27, #000000);
54
+
55
+ /* Type: the font is the game's own (there is no --cwd-font: --cw-font is read with an inherit fallback; set it to use another) */
56
+ --cwd-text-s: 0.8125rem;
57
+ --cwd-text-m: 0.9375rem;
58
+ --cwd-text-l: 1.125rem;
59
+ --cwd-leading: 1.5;
60
+
61
+ /* Shape and space */
62
+ --cwd-radius: 10px;
63
+ --cwd-radius-s: 6px;
64
+ --cwd-border-width: 1px;
65
+ --cwd-space-1: 0.25rem;
66
+ --cwd-space-2: 0.5rem;
67
+ --cwd-space-3: 0.75rem;
68
+ --cwd-space-4: 1rem;
69
+ --cwd-space-5: 1.5rem;
70
+ --cwd-control-height: 2.5rem;
71
+
72
+ /* A dialog is at most this wide (and never wider than the screen), and sits above the game */
73
+ --cwd-dialog-width: 30rem;
74
+ --cwd-layer: 1000;
75
+ }
76
+
77
+ /* The theme prop, or the same attribute on any ancestor, picks one scheme. Without either, the player's setting decides. */
78
+ [data-cw-theme='light'] .cw-player,
79
+ .cw-player[data-cw-theme='light'] {
80
+ color-scheme: light;
81
+ }
82
+ [data-cw-theme='dark'] .cw-player,
83
+ .cw-player[data-cw-theme='dark'] {
84
+ color-scheme: dark;
85
+ }
86
+ /* ---- End of tokens ----------------------------------------------------------------------------------------------- */
87
+
88
+ /* ---- Fallback ---------------------------------------------------------------------------------------------------- */
89
+ /* A browser without light-dark() (before 2024) would drop every colour token above. This gives it the light values; it follows
90
+ neither prefers-color-scheme nor the theme prop. */
91
+ @supports not (color: light-dark(#000, #fff)) {
92
+ :where(.cw-player) {
93
+ --cwd-surface: #ffffff;
94
+ --cwd-surface-sunken: #eef0f4;
95
+ --cwd-border: #e0e3ea;
96
+ --cwd-border-strong: #7a8397;
97
+ --cwd-ink: #151b27;
98
+ --cwd-ink-muted: #4f5a6e;
99
+ --cwd-accent: #2950d3;
100
+ --cwd-accent-ink: #ffffff;
101
+ --cwd-accent-soft: #e7ecfb;
102
+ --cwd-red: #c42f3b;
103
+ --cwd-red-soft: #fbe4e6;
104
+ --cwd-yellow: #e5ad00;
105
+ --cwd-shadow-ink: #151b27;
106
+ }
107
+ }
108
+ /* ---- End of fallback --------------------------------------------------------------------------------------------- */
109
+
110
+ /* ---- Base -------------------------------------------------------------------------------------------------------- */
111
+ .cw-player {
112
+ box-sizing: border-box;
113
+ min-width: 0;
114
+ max-width: 100%;
115
+ margin: 0;
116
+ background: var(--cw-surface, var(--cwd-surface));
117
+ color: var(--cw-ink, var(--cwd-ink));
118
+ font-family: var(--cw-font, inherit);
119
+ font-size: var(--cw-text-m, var(--cwd-text-m));
120
+ line-height: var(--cw-leading, var(--cwd-leading));
121
+ overflow-wrap: anywhere;
122
+ -webkit-text-size-adjust: 100%;
123
+ text-size-adjust: 100%;
124
+ }
125
+ .cw-player *,
126
+ .cw-player *::before,
127
+ .cw-player *::after {
128
+ box-sizing: inherit;
129
+ }
130
+ .cw-player h2,
131
+ .cw-player h3,
132
+ .cw-player p,
133
+ .cw-player ul,
134
+ .cw-player ol,
135
+ .cw-player dl,
136
+ .cw-player dd,
137
+ .cw-player fieldset {
138
+ margin: 0;
139
+ padding: 0;
140
+ }
141
+ .cw-player ul {
142
+ list-style: none;
143
+ }
144
+ .cw-player :is(a, button, input, textarea, [tabindex]):focus-visible {
145
+ outline: 2px solid var(--cw-accent, var(--cwd-accent));
146
+ outline-offset: 2px;
147
+ border-radius: var(--cw-radius-s, var(--cwd-radius-s));
148
+ }
149
+ /* The heading that takes focus is a place to start reading, not a control: no ring of its own. */
150
+ .cw-player .cw-title:focus {
151
+ outline: none;
152
+ }
153
+
154
+ /* ---- The referee card: the same as the moderator hub's -------------------------------------------------------------- */
155
+ .cw-player .cw-card {
156
+ display: inline-block;
157
+ flex: none;
158
+ width: 0.875rem;
159
+ height: 1.1875rem;
160
+ border-radius: 2.5px;
161
+ transform: rotate(-8deg);
162
+ box-shadow: 0 1px 2px color-mix(in srgb, var(--cw-shadow-ink, var(--cwd-shadow-ink)) 25%, transparent);
163
+ }
164
+ .cw-player .cw-card--warning {
165
+ background: var(--cw-yellow, var(--cwd-yellow));
166
+ }
167
+ .cw-player .cw-card--restriction {
168
+ background: linear-gradient(135deg, var(--cw-yellow, var(--cwd-yellow)) 50%, var(--cw-red, var(--cwd-red)) 50%);
169
+ }
170
+ .cw-player .cw-card--ban {
171
+ background: var(--cw-red, var(--cwd-red));
172
+ }
173
+ .cw-player .cw-card--note {
174
+ border: 1.5px dashed var(--cw-ink-muted, var(--cwd-ink-muted));
175
+ background: transparent;
176
+ box-shadow: none;
177
+ }
178
+
179
+ /* ---- Text -------------------------------------------------------------------------------------------------------- */
180
+ .cw-player .cw-title {
181
+ font-size: var(--cw-text-l, var(--cwd-text-l));
182
+ font-weight: 600;
183
+ line-height: 1.25;
184
+ overflow-wrap: anywhere;
185
+ }
186
+ .cw-player .cw-kind {
187
+ font-size: var(--cw-text-m, var(--cwd-text-m));
188
+ font-weight: 600;
189
+ }
190
+ .cw-player .cw-dialog > .cw-title {
191
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
192
+ }
193
+ .cw-player .cw-head {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: var(--cw-space-3, var(--cwd-space-3));
197
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
198
+ }
199
+ .cw-player .cw-body,
200
+ .cw-player .cw-summary {
201
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
202
+ }
203
+ .cw-player .cw-position,
204
+ .cw-player .cw-hint,
205
+ .cw-player .cw-lines-label,
206
+ .cw-player .cw-ref {
207
+ color: var(--cw-ink-muted, var(--cwd-ink-muted));
208
+ font-size: var(--cw-text-s, var(--cwd-text-s));
209
+ }
210
+ .cw-player .cw-position {
211
+ margin-bottom: var(--cw-space-2, var(--cwd-space-2));
212
+ }
213
+ /* Drawn even when empty (it is a live region): an empty one takes no room. */
214
+ .cw-player .cw-position:empty {
215
+ margin: 0;
216
+ }
217
+ .cw-player .cw-hint {
218
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
219
+ }
220
+ .cw-player .cw-state {
221
+ color: var(--cw-ink-muted, var(--cwd-ink-muted));
222
+ }
223
+ .cw-player .cw-section > * + * {
224
+ margin-top: var(--cw-space-3, var(--cwd-space-3));
225
+ }
226
+
227
+ .cw-player .cw-facts {
228
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
229
+ }
230
+ .cw-player .cw-fact + .cw-fact {
231
+ margin-top: var(--cw-space-2, var(--cwd-space-2));
232
+ }
233
+ .cw-player .cw-facts dt {
234
+ color: var(--cw-ink-muted, var(--cwd-ink-muted));
235
+ font-size: var(--cw-text-s, var(--cwd-text-s));
236
+ }
237
+ .cw-player .cw-facts dd {
238
+ overflow-wrap: anywhere;
239
+ }
240
+
241
+ .cw-player .cw-lines {
242
+ display: grid;
243
+ gap: var(--cw-space-2, var(--cwd-space-2));
244
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
245
+ }
246
+ .cw-player .cw-lines li {
247
+ padding: var(--cw-space-2, var(--cwd-space-2)) var(--cw-space-3, var(--cwd-space-3));
248
+ border-left: 3px solid var(--cw-border-strong, var(--cwd-border-strong));
249
+ border-radius: var(--cw-radius-s, var(--cwd-radius-s));
250
+ background: var(--cw-surface-sunken, var(--cwd-surface-sunken));
251
+ }
252
+ .cw-player .cw-line-day {
253
+ display: block;
254
+ color: var(--cw-ink-muted, var(--cwd-ink-muted));
255
+ font-size: var(--cw-text-s, var(--cwd-text-s));
256
+ }
257
+ .cw-player .cw-line-text {
258
+ overflow-wrap: anywhere;
259
+ }
260
+
261
+ .cw-player .cw-rules {
262
+ padding-left: var(--cw-space-5, var(--cwd-space-5));
263
+ }
264
+ .cw-player .cw-rules li + li {
265
+ margin-top: var(--cw-space-3, var(--cwd-space-3));
266
+ }
267
+ .cw-player .cw-rule-text {
268
+ overflow-wrap: anywhere;
269
+ }
270
+
271
+ .cw-player .cw-items {
272
+ display: grid;
273
+ gap: var(--cw-space-3, var(--cwd-space-3));
274
+ }
275
+ .cw-player .cw-item {
276
+ padding: var(--cw-space-4, var(--cwd-space-4));
277
+ border: var(--cw-border-width, var(--cwd-border-width)) solid var(--cw-border, var(--cwd-border));
278
+ border-radius: var(--cw-radius, var(--cwd-radius));
279
+ background: var(--cw-surface, var(--cwd-surface));
280
+ }
281
+
282
+ .cw-player .cw-restricted .cw-text {
283
+ flex: 1 1 10rem;
284
+ min-width: 0;
285
+ }
286
+ .cw-player .cw-restricted {
287
+ display: flex;
288
+ align-items: center;
289
+ flex-wrap: wrap;
290
+ gap: var(--cw-space-2, var(--cwd-space-2)) var(--cw-space-3, var(--cwd-space-3));
291
+ padding: var(--cw-space-2, var(--cwd-space-2)) var(--cw-space-3, var(--cwd-space-3));
292
+ border: var(--cw-border-width, var(--cwd-border-width)) solid var(--cw-border, var(--cwd-border));
293
+ border-radius: var(--cw-radius-s, var(--cwd-radius-s));
294
+ background: var(--cw-surface-sunken, var(--cwd-surface-sunken));
295
+ }
296
+
297
+ /* ---- Links and buttons ------------------------------------------------------------------------------------------- */
298
+ .cw-player .cw-link {
299
+ color: var(--cw-accent, var(--cwd-accent));
300
+ text-decoration: underline;
301
+ text-underline-offset: 0.2em;
302
+ }
303
+ .cw-player .cw-appeal {
304
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
305
+ }
306
+ .cw-player .cw-actions {
307
+ display: flex;
308
+ flex-wrap: wrap;
309
+ gap: var(--cw-space-2, var(--cwd-space-2));
310
+ margin-top: var(--cw-space-4, var(--cwd-space-4));
311
+ }
312
+ .cw-player .cw-btn {
313
+ min-height: var(--cw-control-height, var(--cwd-control-height));
314
+ padding: var(--cw-space-1, var(--cwd-space-1)) var(--cw-space-4, var(--cwd-space-4));
315
+ border: var(--cw-border-width, var(--cwd-border-width)) solid var(--cw-border-strong, var(--cwd-border-strong));
316
+ border-radius: var(--cw-radius-s, var(--cwd-radius-s));
317
+ background: var(--cw-surface, var(--cwd-surface));
318
+ color: var(--cw-ink, var(--cwd-ink));
319
+ font: inherit;
320
+ font-weight: 600;
321
+ white-space: normal;
322
+ cursor: pointer;
323
+ }
324
+ .cw-player .cw-btn:hover {
325
+ background: var(--cw-surface-sunken, var(--cwd-surface-sunken));
326
+ }
327
+ .cw-player .cw-btn--primary {
328
+ border-color: var(--cw-accent, var(--cwd-accent));
329
+ background: var(--cw-accent, var(--cwd-accent));
330
+ color: var(--cw-accent-ink, var(--cwd-accent-ink));
331
+ }
332
+ .cw-player .cw-btn--primary:hover {
333
+ background: var(--cw-accent, var(--cwd-accent));
334
+ filter: brightness(1.08);
335
+ }
336
+ /* A busy button is dimmed and ignores clicks but keeps focus (it is not :disabled, which would drop it). */
337
+ .cw-player .cw-btn[aria-disabled='true'] {
338
+ opacity: 0.6;
339
+ cursor: default;
340
+ }
341
+
342
+ /* ---- Errors ------------------------------------------------------------------------------------------------------ */
343
+ .cw-player .cw-error,
344
+ .cw-player .cw-field-error {
345
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
346
+ padding: var(--cw-space-3, var(--cwd-space-3));
347
+ border: var(--cw-border-width, var(--cwd-border-width)) solid var(--cw-red, var(--cwd-red));
348
+ border-radius: var(--cw-radius-s, var(--cwd-radius-s));
349
+ background: var(--cw-red-soft, var(--cwd-red-soft));
350
+ color: var(--cw-red, var(--cwd-red));
351
+ overflow-wrap: anywhere;
352
+ }
353
+ .cw-player .cw-error .cw-btn {
354
+ margin-top: var(--cw-space-2, var(--cwd-space-2));
355
+ }
356
+
357
+ /* ---- The dialog -------------------------------------------------------------------------------------------------- */
358
+ .cw-player .cw-scrim {
359
+ position: fixed;
360
+ inset: 0;
361
+ z-index: var(--cw-layer, var(--cwd-layer));
362
+ display: grid;
363
+ grid-template-columns: minmax(0, 1fr);
364
+ grid-template-rows: minmax(0, 1fr);
365
+ place-items: center;
366
+ padding: var(--cw-space-3, var(--cwd-space-3));
367
+ background: color-mix(in srgb, var(--cw-shadow-ink, var(--cwd-shadow-ink)) 55%, transparent);
368
+ }
369
+ .cw-player .cw-dialog {
370
+ width: min(var(--cw-dialog-width, var(--cwd-dialog-width)), 100%);
371
+ max-height: 100%;
372
+ overflow-y: auto;
373
+ padding: var(--cw-space-5, var(--cwd-space-5));
374
+ border: var(--cw-border-width, var(--cwd-border-width)) solid var(--cw-border, var(--cwd-border));
375
+ border-radius: var(--cw-radius, var(--cwd-radius));
376
+ background: var(--cw-surface, var(--cwd-surface));
377
+ color: var(--cw-ink, var(--cwd-ink));
378
+ box-shadow: 0 12px 32px color-mix(in srgb, var(--cw-shadow-ink, var(--cwd-shadow-ink)) 35%, transparent);
379
+ animation: cw-in 0.15s ease-out;
380
+ }
381
+ .cw-player .cw-dialog:focus {
382
+ outline: none;
383
+ }
384
+ @keyframes cw-in {
385
+ from {
386
+ opacity: 0;
387
+ transform: translateY(0.5rem);
388
+ }
389
+ to {
390
+ opacity: 1;
391
+ transform: none;
392
+ }
393
+ }
394
+
395
+ /* ---- The report form --------------------------------------------------------------------------------------------- */
396
+ .cw-player .cw-field {
397
+ min-width: 0;
398
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
399
+ }
400
+ .cw-player .cw-field > .cw-hint {
401
+ margin: var(--cw-space-1, var(--cwd-space-1)) 0 0;
402
+ }
403
+ .cw-player .cw-extra {
404
+ min-width: 0;
405
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
406
+ }
407
+ .cw-player .cw-field > .cw-field-error {
408
+ margin: var(--cw-space-2, var(--cwd-space-2)) 0 0;
409
+ }
410
+ .cw-player .cw-quote {
411
+ margin: 0 0 var(--cw-space-3, var(--cwd-space-3));
412
+ padding: var(--cw-space-2, var(--cwd-space-2)) var(--cw-space-3, var(--cwd-space-3));
413
+ border-left: 3px solid var(--cw-border-strong, var(--cwd-border-strong));
414
+ background: var(--cw-surface-sunken, var(--cwd-surface-sunken));
415
+ overflow-wrap: anywhere;
416
+ }
417
+ .cw-player .cw-live:not(:empty) {
418
+ display: grid;
419
+ gap: var(--cw-space-2, var(--cwd-space-2));
420
+ margin-bottom: var(--cw-space-3, var(--cwd-space-3));
421
+ }
422
+ .cw-player .cw-label {
423
+ display: block;
424
+ margin-bottom: var(--cw-space-1, var(--cwd-space-1));
425
+ font-weight: 600;
426
+ }
427
+ .cw-player textarea,
428
+ .cw-player select {
429
+ display: block;
430
+ width: 100%;
431
+ max-width: 100%;
432
+ min-width: 0;
433
+ padding: var(--cw-space-2, var(--cwd-space-2)) var(--cw-space-3, var(--cwd-space-3));
434
+ border: var(--cw-border-width, var(--cwd-border-width)) solid var(--cw-border-strong, var(--cwd-border-strong));
435
+ border-radius: var(--cw-radius-s, var(--cwd-radius-s));
436
+ background: var(--cw-surface, var(--cwd-surface));
437
+ color: var(--cw-ink, var(--cwd-ink));
438
+ font: inherit;
439
+ }
440
+ .cw-player textarea {
441
+ resize: vertical;
442
+ }
443
+ .cw-player select {
444
+ min-height: var(--cw-control-height, var(--cwd-control-height));
445
+ text-overflow: ellipsis;
446
+ }
447
+ /* A reason chosen is a choice you made: the accent, as the old radio was. */
448
+ .cw-player select:has(option:checked:not([value=''])) {
449
+ border-color: var(--cw-accent, var(--cwd-accent));
450
+ background: var(--cw-accent-soft, var(--cwd-accent-soft));
451
+ }
452
+ .cw-player select[aria-invalid='true'] {
453
+ border-color: var(--cw-red, var(--cwd-red));
454
+ }
455
+ /* The one door to the advanced part: a text button, small, in the accent (it is the next action). */
456
+ .cw-player .cw-linkbtn {
457
+ min-height: 2rem;
458
+ margin: 0 0 var(--cw-space-3, var(--cwd-space-3));
459
+ padding: 0;
460
+ border: 0;
461
+ background: none;
462
+ color: var(--cw-accent, var(--cwd-accent));
463
+ font: inherit;
464
+ font-size: var(--cw-text-s, var(--cwd-text-s));
465
+ text-decoration: underline;
466
+ text-underline-offset: 0.2em;
467
+ cursor: pointer;
468
+ }
469
+ /* A thumb needs the same target as the other buttons. */
470
+ @media (pointer: coarse) {
471
+ .cw-player .cw-linkbtn {
472
+ min-height: var(--cw-control-height, var(--cwd-control-height));
473
+ }
474
+ }
475
+ .cw-player textarea + .cw-hint {
476
+ margin-top: var(--cw-space-1, var(--cwd-space-1));
477
+ }
478
+ .cw-player .cw-result {
479
+ padding: var(--cw-space-3, var(--cwd-space-3));
480
+ border: var(--cw-border-width, var(--cwd-border-width)) solid var(--cw-border, var(--cwd-border));
481
+ border-radius: var(--cw-radius-s, var(--cwd-radius-s));
482
+ background: var(--cw-surface-sunken, var(--cwd-surface-sunken));
483
+ }
484
+
485
+ /* ---- Motion and forced colours ----------------------------------------------------------------------------------- */
486
+ @media (prefers-reduced-motion: reduce) {
487
+ .cw-player .cw-dialog {
488
+ animation: none;
489
+ }
490
+ }
491
+ @media (forced-colors: active) {
492
+ .cw-player .cw-card {
493
+ border: 1px solid CanvasText;
494
+ }
495
+ .cw-player .cw-btn,
496
+ .cw-player .cw-dialog,
497
+ .cw-player .cw-item {
498
+ border: 1px solid CanvasText;
499
+ }
500
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * One call to the player routes. `path` is the route path with no prefix (`/notices`); the transport adds whatever prefix its
3
+ * server uses. `query` values that are `undefined` are left out. `body` is sent as JSON.
4
+ *
5
+ * This is the same shape `@chatwarden/hub-react` uses for the staff routes. It is written out here so this package does not
6
+ * depend on that one; a test keeps the two assignable to each other.
7
+ */
8
+ export interface TransportRequest {
9
+ method: string;
10
+ path: string;
11
+ query?: Record<string, string | number | undefined>;
12
+ body?: unknown;
13
+ /** Lets the caller abandon the call (fetch only; the in-page router answers at once). */
14
+ signal?: AbortSignal;
15
+ /** Lets the call finish after the page is gone (fetch `keepalive`). Ignored by the in-page router. */
16
+ keepalive?: boolean;
17
+ }
18
+
19
+ /**
20
+ * What came back: any HTTP status is an answer, not an error. `headers` are lower-case. `body` is the parsed JSON, a string
21
+ * for a text document, or null when there was none.
22
+ */
23
+ export interface TransportResponse {
24
+ status: number;
25
+ headers: Record<string, string>;
26
+ body: unknown;
27
+ }
28
+
29
+ /**
30
+ * How the components reach the player routes. It resolves with the response whatever the status, and rejects only when no
31
+ * answer arrived (a dropped connection, a handler that threw); the client turns that into a `network` error.
32
+ */
33
+ export type Transport = (req: TransportRequest) => Promise<TransportResponse>;
34
+
35
+ /** What `routerTransport` sends to a `createRouter(...)` handler: the kit's `KitRequest`, written out structurally. */
36
+ export interface RouterRequest {
37
+ method: string;
38
+ path: string;
39
+ query: Record<string, string>;
40
+ headers: Record<string, string>;
41
+ body: undefined;
42
+ rawBody?: string;
43
+ }
44
+
45
+ /** What a `createRouter(...)` handler answers: the kit's `KitResponse`, written out structurally. */
46
+ export interface RouterResponse {
47
+ status: number;
48
+ headers?: Record<string, string>;
49
+ body: unknown;
50
+ }
51
+
52
+ /** The query as strings, with `undefined` values dropped. */
53
+ function queryOf(query: TransportRequest['query']): Record<string, string> {
54
+ const out: Record<string, string> = {};
55
+ for (const [key, value] of Object.entries(query ?? {})) if (value !== undefined) out[key] = String(value);
56
+ return out;
57
+ }
58
+
59
+ function lowerCased(headers: Record<string, string> | undefined): Record<string, string> {
60
+ const out: Record<string, string> = {};
61
+ for (const [key, value] of Object.entries(headers ?? {})) out[key.toLowerCase()] = value;
62
+ return out;
63
+ }
64
+
65
+ export interface RouterTransportOptions {
66
+ /** Sent with every request, for an `identify` hook that reads a header. */
67
+ headers?: Record<string, string>;
68
+ }
69
+
70
+ /**
71
+ * A transport that calls `createRouter(playerRoutes(...))` directly, in the same process: no network. A demo and this
72
+ * package's tests use it, so the components run the real player routes. A response body goes through JSON on the way out, so
73
+ * the components see exactly what a fetch would deliver.
74
+ */
75
+ export function routerTransport(handle: (req: RouterRequest) => Promise<RouterResponse>, options: RouterTransportOptions = {}): Transport {
76
+ return async (req) => {
77
+ const request: RouterRequest = {
78
+ method: req.method,
79
+ path: req.path,
80
+ query: queryOf(req.query),
81
+ headers: { ...options.headers },
82
+ body: undefined,
83
+ };
84
+ // The routes read `rawBody`, never `body` (a pre-parsed body would skip the size cap and the key check).
85
+ if (req.body !== undefined) request.rawBody = JSON.stringify(req.body);
86
+ const res = await handle(request);
87
+ const body: unknown = typeof res.body === 'string' ? res.body : res.body === undefined ? null : (JSON.parse(JSON.stringify(res.body)) as unknown);
88
+ return { status: res.status, headers: lowerCased(res.headers), body };
89
+ };
90
+ }
91
+
92
+ export interface FetchTransportOptions {
93
+ /** Where the player routes are mounted: `https://host/player` or `/player`. No trailing slash needed. */
94
+ baseUrl: string;
95
+ /** Sent with every request (a session token, for example). */
96
+ headers?: Record<string, string>;
97
+ /** `include` sends cookies to another origin. Default: the browser's (`same-origin`). */
98
+ credentials?: RequestCredentials;
99
+ /** For tests, or a polyfill. Default: the global `fetch`. */
100
+ fetch?: typeof fetch;
101
+ }
102
+
103
+ /** The default transport for a real game: `fetch` against the routes your server mounts. */
104
+ export function fetchTransport(options: FetchTransportOptions): Transport {
105
+ if (typeof options?.baseUrl !== 'string') throw new TypeError('fetchTransport: baseUrl is required (for example "/player")');
106
+ // "/" and "" both mean the server root: routes are then "/notices", "/record", ...
107
+ const base = options.baseUrl.replace(/\/+$/, '');
108
+ return async (req) => {
109
+ const params = new URLSearchParams(queryOf(req.query)).toString();
110
+ const url = `${base}${req.path}${params === '' ? '' : `?${params}`}`;
111
+ const headers: Record<string, string> = { accept: 'application/json', ...options.headers };
112
+ const init: RequestInit = { method: req.method, headers };
113
+ if (req.body !== undefined) {
114
+ headers['content-type'] = 'application/json';
115
+ init.body = JSON.stringify(req.body);
116
+ }
117
+ if (options.credentials !== undefined) init.credentials = options.credentials;
118
+ if (req.signal !== undefined) init.signal = req.signal;
119
+ if (req.keepalive === true) init.keepalive = true;
120
+ const res = await (options.fetch ?? globalThis.fetch)(url, init);
121
+ const headersOut: Record<string, string> = {};
122
+ res.headers.forEach((value, key) => (headersOut[key.toLowerCase()] = value));
123
+ const text = await res.text();
124
+ let body: unknown = text === '' ? null : text;
125
+ if (text !== '' && /\bjson\b/i.test(headersOut['content-type'] ?? '')) {
126
+ try {
127
+ body = JSON.parse(text) as unknown;
128
+ } catch {
129
+ body = text;
130
+ }
131
+ }
132
+ return { status: res.status, headers: headersOut, body };
133
+ };
134
+ }