@hadoku/prefs-client 1.1.3 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,6 +97,28 @@ const client = createPrefsClient({
97
97
  });
98
98
  ```
99
99
 
100
+ ## Platform rows (`@wolffm/prefs-client/platform`)
101
+
102
+ Rows that belong to no single app. Import the definition rather than retyping the appId,
103
+ scope and schema, so the second app to need a setting finds the first one's row instead of
104
+ inventing its own.
105
+
106
+ | Row | Fields | Scope | Owner |
107
+ | --------------------- | ------------------------ | ------ | ---------------------------------------------------------- |
108
+ | `games` (`GAMES_ROW`) | `muted`, `volume` (0..1) | device | every hadoku game: master sound, above a game's own levels |
109
+ | `portfolio` | `theme` | device | `@wolffm/themes`; do not write it from an app |
110
+
111
+ ```ts
112
+ import { createPrefsClient } from '@wolffm/prefs-client';
113
+ import { GAMES_ROW, GAMES_DEFAULTS } from '@wolffm/prefs-client/platform';
114
+
115
+ const games = createPrefsClient({ ...GAMES_ROW, defaults: GAMES_DEFAULTS });
116
+ await games.save({ muted: true }, { scope: GAMES_ROW.scope });
117
+ ```
118
+
119
+ The profile picture is not a row here: it is `./avatar` below, whose fallback is the
120
+ watchparty character.
121
+
100
122
  ## Avatars (`@wolffm/prefs-client/avatar`)
101
123
 
102
124
  One resolved avatar per hadoku user, chosen by the user at `hadoku.me/account`.
package/dist/avatar.d.ts CHANGED
@@ -22,8 +22,8 @@ type LinkProvider = (typeof LINK_PROVIDERS)[number];
22
22
  * square, at least 184px. Render it as an `<img>`; never fetch it server-side
23
23
  * on a user's behalf.
24
24
  *
25
- * A source that stops being available (an unlink, an admin removal, a Discord
26
- * avatar that changed) resolves to `sprite` with no action from the app.
25
+ * A source that stops being available (an unlink, an admin removal, an account
26
+ * whose custom avatar was removed) resolves to `sprite` with no action from the app.
27
27
  */
28
28
  type ResolvedAvatar = {
29
29
  source: 'sprite';
@@ -36,12 +36,10 @@ type ResolvedAvatar = {
36
36
  };
37
37
  /** A linked account as its owner sees it. Never carries a token. */
38
38
  interface LinkedAccountView {
39
- /** null when the account has no custom avatar, or the cached one went stale. */
39
+ /** null when the account has no custom avatar. */
40
40
  avatarUrl: string | null;
41
41
  linkedAt: number;
42
42
  refreshedAt: number;
43
- /** Set when Discord stopped serving the cached avatar: link again to pick up the new one. */
44
- stale: boolean;
45
43
  }
46
44
  interface UploadView {
47
45
  url: string;
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Platform rows — prefs rows that belong to no single app, so every app that
5
+ * reads or writes one imports its definition from here rather than retyping
6
+ * the appId, the scope and the schema.
7
+ *
8
+ * A row here is only a NAME and a SHAPE: prefs-api accepts any `[a-z0-9-]+`
9
+ * appId and stores the blob as given, so nothing server-side changes when one
10
+ * is added. What this file stops is the second app inventing its own row for
11
+ * the same setting (bomberman's `bomberman.volume` in localStorage beside
12
+ * cardtable's `games` row, 2026-10-30).
13
+ *
14
+ * Not here, and why:
15
+ * - Theme (`portfolio`, device scope) is owned by `@wolffm/themes`, which
16
+ * renders the picker for every app.
17
+ * - The profile picture is `./avatar`: one resolved avatar per user, whose
18
+ * fallback is the watchparty character read from the `watchparty` row.
19
+ *
20
+ * Usage:
21
+ * createPrefsClient({ ...GAMES_ROW, defaults: GAMES_DEFAULTS })
22
+ * prefs.save({ volume: 0.4 }, { scope: GAMES_ROW.scope })
23
+ */
24
+
25
+ /**
26
+ * Master sound for every hadoku game. Device scope: the right volume belongs
27
+ * to the speakers, not the account. A game's own levels (music, effects) stay
28
+ * in its own row; these two sit above them.
29
+ */
30
+ declare const GAMES_ROW: {
31
+ readonly appId: "games";
32
+ readonly scope: "device";
33
+ readonly schema: z.ZodObject<{
34
+ muted: z.ZodOptional<z.ZodBoolean>;
35
+ volume: z.ZodOptional<z.ZodNumber>;
36
+ }, z.core.$strip>;
37
+ };
38
+ type GamesPrefs = z.infer<typeof GAMES_ROW.schema>;
39
+ declare const GAMES_DEFAULTS: Required<GamesPrefs>;
40
+
41
+ export { GAMES_DEFAULTS, GAMES_ROW, type GamesPrefs };
@@ -0,0 +1,17 @@
1
+ // src/platform.ts
2
+ import { z } from "zod";
3
+ var GAMES_ROW = {
4
+ appId: "games",
5
+ scope: "device",
6
+ schema: z.object({
7
+ /** Silences the game entirely; `volume` is kept for when it is unmuted. */
8
+ muted: z.boolean().optional(),
9
+ /** Master volume, 0 (silent) to 1 (full). */
10
+ volume: z.number().min(0).max(1).optional()
11
+ })
12
+ };
13
+ var GAMES_DEFAULTS = { muted: false, volume: 1 };
14
+ export {
15
+ GAMES_DEFAULTS,
16
+ GAMES_ROW
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadoku/prefs-client",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Client SDK for the hadoku unified preferences store. Wraps /prefs/api/v1/* with localStorage caching, debounced writes, Zod-validated reads, and a usePrefs() React hook, plus match history, ratings and leaderboards for every game (./games), and the one resolved avatar per user (./avatar).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,6 +18,10 @@
18
18
  "types": "./dist/games.d.ts",
19
19
  "default": "./dist/games.js"
20
20
  },
21
+ "./platform": {
22
+ "types": "./dist/platform.d.ts",
23
+ "default": "./dist/platform.js"
24
+ },
21
25
  "./avatar": {
22
26
  "types": "./dist/avatar.d.ts",
23
27
  "default": "./dist/avatar.js"