cursedbelt-core 1.0.0

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 (307) hide show
  1. package/LICENSE +15 -0
  2. package/dist/client/wsWire.d.ts +49 -0
  3. package/dist/client/wsWire.js +57 -0
  4. package/dist/config/appConfig.d.ts +361 -0
  5. package/dist/config/appConfig.js +308 -0
  6. package/dist/core/activity/index.d.ts +8 -0
  7. package/dist/core/activity/index.js +7 -0
  8. package/dist/core/activity/model.d.ts +115 -0
  9. package/dist/core/activity/model.js +63 -0
  10. package/dist/core/analytics/index.d.ts +18 -0
  11. package/dist/core/analytics/index.js +18 -0
  12. package/dist/core/analytics/range.d.ts +186 -0
  13. package/dist/core/analytics/range.js +271 -0
  14. package/dist/core/analytics/schedule.d.ts +301 -0
  15. package/dist/core/analytics/schedule.js +372 -0
  16. package/dist/core/category/index.d.ts +3 -0
  17. package/dist/core/category/index.js +6 -0
  18. package/dist/core/category/memoryStore.d.ts +21 -0
  19. package/dist/core/category/memoryStore.js +97 -0
  20. package/dist/core/category/tree.d.ts +26 -0
  21. package/dist/core/category/tree.js +113 -0
  22. package/dist/core/category/types.d.ts +48 -0
  23. package/dist/core/category/types.js +8 -0
  24. package/dist/core/color.d.ts +14 -0
  25. package/dist/core/color.js +32 -0
  26. package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
  27. package/dist/core/colorScheme/colorSchemeStore.js +123 -0
  28. package/dist/core/colorScheme/index.d.ts +7 -0
  29. package/dist/core/colorScheme/index.js +7 -0
  30. package/dist/core/domainOwners.d.ts +19 -0
  31. package/dist/core/domainOwners.js +15 -0
  32. package/dist/core/download.d.ts +14 -0
  33. package/dist/core/download.js +37 -0
  34. package/dist/core/events/ccEventBus.d.ts +44 -0
  35. package/dist/core/events/ccEventBus.js +70 -0
  36. package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
  37. package/dist/core/file-tree/fileTreeModel.js +312 -0
  38. package/dist/core/folder-tree/actions.d.ts +84 -0
  39. package/dist/core/folder-tree/actions.js +119 -0
  40. package/dist/core/folder-tree/index.d.ts +3 -0
  41. package/dist/core/folder-tree/index.js +20 -0
  42. package/dist/core/folder-tree/path.d.ts +76 -0
  43. package/dist/core/folder-tree/path.js +146 -0
  44. package/dist/core/folder-tree/tree.d.ts +86 -0
  45. package/dist/core/folder-tree/tree.js +184 -0
  46. package/dist/core/layout/index.d.ts +1 -0
  47. package/dist/core/layout/index.js +7 -0
  48. package/dist/core/master-lock/index.d.ts +14 -0
  49. package/dist/core/master-lock/index.js +14 -0
  50. package/dist/core/master-lock/kdf.d.ts +63 -0
  51. package/dist/core/master-lock/kdf.js +117 -0
  52. package/dist/core/master-lock/policy.d.ts +32 -0
  53. package/dist/core/master-lock/policy.js +52 -0
  54. package/dist/core/master-lock/presence.d.ts +52 -0
  55. package/dist/core/master-lock/presence.js +61 -0
  56. package/dist/core/master-lock/wire.d.ts +139 -0
  57. package/dist/core/master-lock/wire.js +81 -0
  58. package/dist/core/media/bgRemoval.d.ts +27 -0
  59. package/dist/core/media/bgRemoval.js +101 -0
  60. package/dist/core/media/ffmpeg.d.ts +57 -0
  61. package/dist/core/media/ffmpeg.js +126 -0
  62. package/dist/core/media/format.d.ts +20 -0
  63. package/dist/core/media/format.js +54 -0
  64. package/dist/core/media/index.d.ts +12 -0
  65. package/dist/core/media/index.js +18 -0
  66. package/dist/core/media/mediaItem.d.ts +15 -0
  67. package/dist/core/media/mediaItem.js +34 -0
  68. package/dist/core/media/renderPlan.d.ts +63 -0
  69. package/dist/core/media/renderPlan.js +232 -0
  70. package/dist/core/media/segment.d.ts +79 -0
  71. package/dist/core/media/segment.js +247 -0
  72. package/dist/core/media/size.d.ts +1 -0
  73. package/dist/core/media/size.js +3 -0
  74. package/dist/core/media/trim.d.ts +21 -0
  75. package/dist/core/media/trim.js +40 -0
  76. package/dist/core/media/types.d.ts +148 -0
  77. package/dist/core/media/types.js +11 -0
  78. package/dist/core/media/uploadLimits.d.ts +7 -0
  79. package/dist/core/media/uploadLimits.js +16 -0
  80. package/dist/core/media/validate.d.ts +9 -0
  81. package/dist/core/media/validate.js +121 -0
  82. package/dist/core/media/videoCuts.d.ts +152 -0
  83. package/dist/core/media/videoCuts.js +284 -0
  84. package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
  85. package/dist/core/navigation/breadcrumbCompat.js +21 -0
  86. package/dist/core/navigation/coverageTest.d.ts +24 -0
  87. package/dist/core/navigation/coverageTest.js +48 -0
  88. package/dist/core/navigation/index.d.ts +6 -0
  89. package/dist/core/navigation/index.js +10 -0
  90. package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
  91. package/dist/core/navigation/migrateNavPreferences.js +63 -0
  92. package/dist/core/navigation/navigationModel.d.ts +165 -0
  93. package/dist/core/navigation/navigationModel.js +14 -0
  94. package/dist/core/navigation/navigationSelectors.d.ts +63 -0
  95. package/dist/core/navigation/navigationSelectors.js +167 -0
  96. package/dist/core/navigation/searchCatalogue.d.ts +23 -0
  97. package/dist/core/navigation/searchCatalogue.js +76 -0
  98. package/dist/core/net/reconnectSchedule.d.ts +46 -0
  99. package/dist/core/net/reconnectSchedule.js +38 -0
  100. package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
  101. package/dist/core/palette/ShortcutRegistry.js +52 -0
  102. package/dist/core/palette/fuzzyRank.d.ts +18 -0
  103. package/dist/core/palette/fuzzyRank.js +73 -0
  104. package/dist/core/palette/hotkeys.d.ts +28 -0
  105. package/dist/core/palette/hotkeys.js +118 -0
  106. package/dist/core/palette/index.d.ts +3 -0
  107. package/dist/core/palette/index.js +8 -0
  108. package/dist/core/schema/projection.d.ts +84 -0
  109. package/dist/core/schema/projection.js +184 -0
  110. package/dist/core/session/claims.d.ts +145 -0
  111. package/dist/core/session/claims.js +157 -0
  112. package/dist/core/sharing/index.d.ts +5 -0
  113. package/dist/core/sharing/index.js +4 -0
  114. package/dist/core/sharing/model.d.ts +195 -0
  115. package/dist/core/sharing/model.js +150 -0
  116. package/dist/core/site/siteCopy.d.ts +92 -0
  117. package/dist/core/site/siteCopy.js +91 -0
  118. package/dist/core/slots.d.ts +22 -0
  119. package/dist/core/slots.js +31 -0
  120. package/dist/core/styles/constructableSheet.d.ts +51 -0
  121. package/dist/core/styles/constructableSheet.js +80 -0
  122. package/dist/core/tokens/registry.d.ts +119 -0
  123. package/dist/core/tokens/registry.js +447 -0
  124. package/dist/core/uiScale/index.d.ts +8 -0
  125. package/dist/core/uiScale/index.js +8 -0
  126. package/dist/core/uiScale/scale.d.ts +36 -0
  127. package/dist/core/uiScale/scale.js +47 -0
  128. package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
  129. package/dist/core/uiScale/uiScaleStore.js +63 -0
  130. package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
  131. package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
  132. package/dist/core/wire/index.d.ts +17 -0
  133. package/dist/core/wire/index.js +17 -0
  134. package/dist/core/wire/ingress.d.ts +44 -0
  135. package/dist/core/wire/ingress.js +134 -0
  136. package/dist/core/wire/marshalWorker.d.ts +70 -0
  137. package/dist/core/wire/marshalWorker.js +273 -0
  138. package/dist/core/wire/marshalling.d.ts +122 -0
  139. package/dist/core/wire/marshalling.js +126 -0
  140. package/dist/core/wire/wireHub.d.ts +80 -0
  141. package/dist/core/wire/wireHub.js +116 -0
  142. package/dist/ctgr/base64url.d.ts +8 -0
  143. package/dist/ctgr/base64url.js +39 -0
  144. package/dist/ctgr/chunkAccumulator.d.ts +214 -0
  145. package/dist/ctgr/chunkAccumulator.js +307 -0
  146. package/dist/ctgr/chunkStore.d.ts +24 -0
  147. package/dist/ctgr/chunkStore.js +125 -0
  148. package/dist/ctgr/codec.d.ts +44 -0
  149. package/dist/ctgr/codec.js +153 -0
  150. package/dist/ctgr/compress.d.ts +2 -0
  151. package/dist/ctgr/compress.js +20 -0
  152. package/dist/ctgr/crc32.d.ts +2 -0
  153. package/dist/ctgr/crc32.js +23 -0
  154. package/dist/ctgr/digest.d.ts +1 -0
  155. package/dist/ctgr/digest.js +15 -0
  156. package/dist/ctgr/index.d.ts +9 -0
  157. package/dist/ctgr/index.js +17 -0
  158. package/dist/ctgr/types.d.ts +64 -0
  159. package/dist/ctgr/types.js +21 -0
  160. package/dist/ctgr/v0compat.d.ts +27 -0
  161. package/dist/ctgr/v0compat.js +128 -0
  162. package/dist/index.d.ts +15 -0
  163. package/dist/index.js +17 -0
  164. package/dist/shared/errors.d.ts +101 -0
  165. package/dist/shared/errors.js +99 -0
  166. package/dist/testing/bytes.d.ts +32 -0
  167. package/dist/testing/bytes.js +33 -0
  168. package/dist/testing/domInspect.d.ts +59 -0
  169. package/dist/testing/domInspect.js +115 -0
  170. package/dist/testing/fakeFetch.d.ts +23 -0
  171. package/dist/testing/fakeFetch.js +21 -0
  172. package/dist/testing/index.d.ts +3 -0
  173. package/dist/testing/index.js +11 -0
  174. package/package.json +373 -0
  175. package/src/client/wsWire.ts +83 -0
  176. package/src/config/appConfig.spec.ts +158 -0
  177. package/src/config/appConfig.ts +372 -0
  178. package/src/core/activity/index.ts +21 -0
  179. package/src/core/activity/model.ts +160 -0
  180. package/src/core/analytics/index.ts +18 -0
  181. package/src/core/analytics/range.spec.ts +280 -0
  182. package/src/core/analytics/range.ts +383 -0
  183. package/src/core/analytics/schedule.spec.ts +311 -0
  184. package/src/core/analytics/schedule.ts +624 -0
  185. package/src/core/category/category.spec.ts +144 -0
  186. package/src/core/category/index.ts +19 -0
  187. package/src/core/category/memoryStore.ts +104 -0
  188. package/src/core/category/tree.ts +118 -0
  189. package/src/core/category/types.ts +59 -0
  190. package/src/core/color.spec.ts +35 -0
  191. package/src/core/color.ts +35 -0
  192. package/src/core/colorScheme/colorScheme.spec.ts +127 -0
  193. package/src/core/colorScheme/colorSchemeStore.ts +184 -0
  194. package/src/core/colorScheme/index.ts +23 -0
  195. package/src/core/domainOwners.ts +32 -0
  196. package/src/core/download.spec.ts +38 -0
  197. package/src/core/download.ts +35 -0
  198. package/src/core/events/ccEventBus.spec.ts +90 -0
  199. package/src/core/events/ccEventBus.ts +120 -0
  200. package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
  201. package/src/core/file-tree/fileTreeModel.ts +511 -0
  202. package/src/core/folder-tree/actions.spec.ts +103 -0
  203. package/src/core/folder-tree/actions.ts +153 -0
  204. package/src/core/folder-tree/index.ts +46 -0
  205. package/src/core/folder-tree/path.spec.ts +113 -0
  206. package/src/core/folder-tree/path.ts +148 -0
  207. package/src/core/folder-tree/tree.spec.ts +157 -0
  208. package/src/core/folder-tree/tree.ts +244 -0
  209. package/src/core/layout/index.ts +7 -0
  210. package/src/core/master-lock/index.ts +50 -0
  211. package/src/core/master-lock/kdf.spec.ts +95 -0
  212. package/src/core/master-lock/kdf.ts +145 -0
  213. package/src/core/master-lock/policy.ts +55 -0
  214. package/src/core/master-lock/presence.spec.ts +73 -0
  215. package/src/core/master-lock/presence.ts +71 -0
  216. package/src/core/master-lock/wire.ts +155 -0
  217. package/src/core/media/bgRemoval.spec.ts +120 -0
  218. package/src/core/media/bgRemoval.ts +123 -0
  219. package/src/core/media/ffmpeg.spec.ts +135 -0
  220. package/src/core/media/ffmpeg.ts +162 -0
  221. package/src/core/media/format.spec.ts +41 -0
  222. package/src/core/media/format.ts +58 -0
  223. package/src/core/media/index.ts +105 -0
  224. package/src/core/media/mediaItem.spec.ts +46 -0
  225. package/src/core/media/mediaItem.ts +40 -0
  226. package/src/core/media/renderPlan.spec.ts +140 -0
  227. package/src/core/media/renderPlan.ts +318 -0
  228. package/src/core/media/segment.spec.ts +159 -0
  229. package/src/core/media/segment.ts +352 -0
  230. package/src/core/media/size.spec.ts +19 -0
  231. package/src/core/media/size.ts +3 -0
  232. package/src/core/media/trim.spec.ts +75 -0
  233. package/src/core/media/trim.ts +60 -0
  234. package/src/core/media/types.ts +147 -0
  235. package/src/core/media/uploadLimits.ts +18 -0
  236. package/src/core/media/validate.spec.ts +106 -0
  237. package/src/core/media/validate.ts +131 -0
  238. package/src/core/media/videoCuts.spec.ts +327 -0
  239. package/src/core/media/videoCuts.ts +355 -0
  240. package/src/core/navigation/breadcrumbCompat.ts +44 -0
  241. package/src/core/navigation/coverageTest.ts +54 -0
  242. package/src/core/navigation/index.ts +50 -0
  243. package/src/core/navigation/migrateNavPreferences.ts +119 -0
  244. package/src/core/navigation/navigation.spec.ts +388 -0
  245. package/src/core/navigation/navigationModel.ts +185 -0
  246. package/src/core/navigation/navigationSelectors.ts +206 -0
  247. package/src/core/navigation/searchCatalogue.ts +89 -0
  248. package/src/core/net/reconnectSchedule.spec.ts +49 -0
  249. package/src/core/net/reconnectSchedule.ts +78 -0
  250. package/src/core/palette/ShortcutRegistry.ts +69 -0
  251. package/src/core/palette/fuzzyRank.ts +78 -0
  252. package/src/core/palette/hotkeys.ts +132 -0
  253. package/src/core/palette/index.ts +22 -0
  254. package/src/core/palette/palette.spec.ts +160 -0
  255. package/src/core/schema/projection.spec.ts +122 -0
  256. package/src/core/schema/projection.ts +225 -0
  257. package/src/core/session/claims.ts +181 -0
  258. package/src/core/sharing/index.ts +33 -0
  259. package/src/core/sharing/model.spec.ts +104 -0
  260. package/src/core/sharing/model.ts +296 -0
  261. package/src/core/site/siteCopy.ts +115 -0
  262. package/src/core/slots.ts +38 -0
  263. package/src/core/styles/constructableSheet.ts +86 -0
  264. package/src/core/tokens/registry.spec.ts +160 -0
  265. package/src/core/tokens/registry.ts +536 -0
  266. package/src/core/uiScale/index.ts +32 -0
  267. package/src/core/uiScale/scale.ts +63 -0
  268. package/src/core/uiScale/uiScale.spec.ts +77 -0
  269. package/src/core/uiScale/uiScaleStore.ts +100 -0
  270. package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
  271. package/src/core/wire/index.ts +56 -0
  272. package/src/core/wire/ingress.spec.ts +276 -0
  273. package/src/core/wire/ingress.ts +182 -0
  274. package/src/core/wire/marshalWorker.spec.ts +196 -0
  275. package/src/core/wire/marshalWorker.ts +361 -0
  276. package/src/core/wire/marshalling.spec.ts +139 -0
  277. package/src/core/wire/marshalling.ts +238 -0
  278. package/src/core/wire/wireHub.spec.ts +137 -0
  279. package/src/core/wire/wireHub.ts +213 -0
  280. package/src/ctgr/base64url.ts +44 -0
  281. package/src/ctgr/chunkAccumulator.ts +403 -0
  282. package/src/ctgr/chunkStore.spec.ts +101 -0
  283. package/src/ctgr/chunkStore.ts +168 -0
  284. package/src/ctgr/codec.spec.ts +208 -0
  285. package/src/ctgr/codec.ts +202 -0
  286. package/src/ctgr/compress.ts +21 -0
  287. package/src/ctgr/crc32.ts +23 -0
  288. package/src/ctgr/digest.ts +14 -0
  289. package/src/ctgr/index.ts +39 -0
  290. package/src/ctgr/types.ts +80 -0
  291. package/src/ctgr/v0compat.spec.ts +73 -0
  292. package/src/ctgr/v0compat.ts +156 -0
  293. package/src/declaredDepsAreImported.spec.ts +87 -0
  294. package/src/index.ts +25 -0
  295. package/src/leafSubpathsImportNothing.spec.ts +287 -0
  296. package/src/lockfileIsTracked.spec.ts +50 -0
  297. package/src/namedSubpathsResolve.spec.ts +122 -0
  298. package/src/noPathDeps.spec.ts +24 -0
  299. package/src/publishShape.spec.ts +89 -0
  300. package/src/shared/errors.ts +153 -0
  301. package/src/skippedTestsAreAnswered.spec.ts +221 -0
  302. package/src/testTmpRoot.spec.ts +102 -0
  303. package/src/testing/bytes.ts +38 -0
  304. package/src/testing/domInspect.spec.ts +79 -0
  305. package/src/testing/domInspect.ts +127 -0
  306. package/src/testing/fakeFetch.ts +26 -0
  307. package/src/testing/index.ts +11 -0
@@ -0,0 +1,184 @@
1
+ /**
2
+ * The color-scheme store: holds the current light/dark preference, persists it,
3
+ * applies it to the document root (the `.dark` class from `theme.css`'s contract),
4
+ * and notifies subscribers. Split from the React hook so it works before React
5
+ * mounts (call {@link initColorScheme} early to apply the saved scheme with no
6
+ * flash) and so its logic is testable without a DOM.
7
+ *
8
+ * Three modes, two resolved schemes: `'system'` (the default when nothing is
9
+ * stored) follows `prefers-color-scheme` live; picking light/dark via the toggle
10
+ * stores an explicit override. Side-effects (storage, applying to the page, the
11
+ * OS preference) are injected, so a test drives a real store with fakes; the
12
+ * exported default singleton binds them to `localStorage`, the `<html>` element,
13
+ * and `matchMedia`.
14
+ */
15
+
16
+ /** What the user chose. `'system'` = no explicit choice, follow the OS. */
17
+ export type ColorSchemeMode = 'light' | 'dark' | 'system';
18
+ /** What the page actually renders. */
19
+ export type ResolvedColorScheme = 'light' | 'dark';
20
+
21
+ /** The slice of the Web Storage API the store needs (so a test can fake it). */
22
+ export interface ColorSchemeStorage {
23
+ getItem(key: string): string | null;
24
+ setItem(key: string, value: string): void;
25
+ removeItem(key: string): void;
26
+ }
27
+
28
+ export interface ColorSchemeStore {
29
+ /** Current mode (lazily hydrated from storage on first read). */
30
+ getMode(): ColorSchemeMode;
31
+ /** The scheme the page renders: the mode, or the OS preference under `'system'`. */
32
+ getResolved(): ResolvedColorScheme;
33
+ /** Set, persist, apply, and notify. `'system'` clears the stored override. */
34
+ setMode(mode: ColorSchemeMode): void;
35
+ /** Flip to the opposite of the currently *resolved* scheme (always explicit). */
36
+ toggle(): void;
37
+ /** Apply the persisted scheme to the page + start following OS changes while in
38
+ * `'system'` mode (call once on app boot). Idempotent. */
39
+ init(): void;
40
+ subscribe(listener: () => void): () => void;
41
+ }
42
+
43
+ export interface ColorSchemeStoreOptions {
44
+ storage?: ColorSchemeStorage | null;
45
+ /** Apply a resolved scheme to the page. Default toggles `.dark` on `<html>`. */
46
+ apply?: (scheme: ResolvedColorScheme) => void;
47
+ /** Read the OS preference (default: `matchMedia('(prefers-color-scheme: dark)')`). */
48
+ getSystemScheme?: () => ResolvedColorScheme;
49
+ /** Subscribe to OS preference changes; returns an unsubscribe. */
50
+ onSystemChange?: (listener: () => void) => () => void;
51
+ /** Subscribe to the root element's `.dark` class being changed by something OTHER
52
+ * than this store (devtools, a screenshot/visual-test harness driving the DOM
53
+ * directly). Returns an unsubscribe. Default: a `MutationObserver` on
54
+ * `document.documentElement`'s `class`/`style` attributes. */
55
+ onExternalChange?: (listener: () => void) => () => void;
56
+ /** localStorage key. */
57
+ key?: string;
58
+ }
59
+
60
+ export const COLOR_SCHEME_STORAGE_KEY = 'cwip.colorScheme';
61
+
62
+ const isMode = (v: unknown): v is Exclude<ColorSchemeMode, 'system'> =>
63
+ v === 'light' || v === 'dark';
64
+
65
+ export function createColorSchemeStore(options: ColorSchemeStoreOptions = {}): ColorSchemeStore {
66
+ const {
67
+ storage = null,
68
+ apply,
69
+ getSystemScheme = () => 'light',
70
+ onSystemChange,
71
+ onExternalChange,
72
+ key = COLOR_SCHEME_STORAGE_KEY,
73
+ } = options;
74
+ const listeners = new Set<() => void>();
75
+ let mode: ColorSchemeMode | undefined; // undefined until first hydrate
76
+ let watchingSystem = false;
77
+ let watchingExternal = false;
78
+
79
+ const hydrate = (): ColorSchemeMode => {
80
+ if (mode !== undefined) return mode;
81
+ const raw = storage?.getItem(key);
82
+ mode = isMode(raw) ? raw : 'system';
83
+ return mode;
84
+ };
85
+
86
+ const resolve = (): ResolvedColorScheme => {
87
+ const m = hydrate();
88
+ return m === 'system' ? getSystemScheme() : m;
89
+ };
90
+
91
+ const notify = (): void => {
92
+ for (const l of listeners) l();
93
+ };
94
+
95
+ const commit = (next: ColorSchemeMode): void => {
96
+ mode = next;
97
+ if (next === 'system') storage?.removeItem(key);
98
+ else storage?.setItem(key, next);
99
+ apply?.(resolve());
100
+ notify();
101
+ };
102
+
103
+ return {
104
+ getMode: hydrate,
105
+ getResolved: resolve,
106
+ setMode: commit,
107
+ toggle: () => commit(resolve() === 'dark' ? 'light' : 'dark'),
108
+ init() {
109
+ apply?.(resolve());
110
+ if (!watchingSystem && onSystemChange) {
111
+ watchingSystem = true;
112
+ onSystemChange(() => {
113
+ // Only the resolved scheme changed; re-apply + notify while following the OS.
114
+ if (hydrate() === 'system') {
115
+ apply?.(resolve());
116
+ notify();
117
+ }
118
+ });
119
+ }
120
+ if (!watchingExternal && onExternalChange) {
121
+ watchingExternal = true;
122
+ onExternalChange(() => {
123
+ // Something outside this store (devtools, a visual-test harness) touched
124
+ // the root's `.dark` class/localStorage directly — drop the cached mode
125
+ // so the next read re-hydrates from storage, and tell every subscriber
126
+ // (the theme toggle, etc.) to re-check so they can't render stale.
127
+ mode = undefined;
128
+ notify();
129
+ });
130
+ }
131
+ },
132
+ subscribe(listener) {
133
+ listeners.add(listener);
134
+ return () => listeners.delete(listener);
135
+ },
136
+ };
137
+ }
138
+
139
+ /** Apply a resolved scheme to the document root via the `theme.css` contract:
140
+ * the `.dark` class on `<html>` (never `data-theme`, never the media query —
141
+ * `dark:` utilities and every token consumer key off this class). SSR-safe. */
142
+ export function applyColorSchemeToDocument(scheme: ResolvedColorScheme): void {
143
+ if (typeof document === 'undefined') return;
144
+ document.documentElement.classList.toggle('dark', scheme === 'dark');
145
+ // Hint native form controls/scrollbars so they match the app chrome.
146
+ document.documentElement.style.colorScheme = scheme;
147
+ }
148
+
149
+ const defaultStorage: ColorSchemeStorage | null =
150
+ typeof localStorage !== 'undefined' ? localStorage : null;
151
+
152
+ const darkQuery = (): MediaQueryList | null =>
153
+ typeof window !== 'undefined' && typeof window.matchMedia === 'function'
154
+ ? window.matchMedia('(prefers-color-scheme: dark)')
155
+ : null;
156
+
157
+ /** The app-wide singleton bound to localStorage + the page + the OS preference. */
158
+ export const colorSchemeStore = createColorSchemeStore({
159
+ storage: defaultStorage,
160
+ apply: applyColorSchemeToDocument,
161
+ getSystemScheme: () => (darkQuery()?.matches ? 'dark' : 'light'),
162
+ onSystemChange: (listener) => {
163
+ const q = darkQuery();
164
+ if (!q) return () => {};
165
+ q.addEventListener('change', listener);
166
+ return () => q.removeEventListener('change', listener);
167
+ },
168
+ onExternalChange: (listener) => {
169
+ if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return () => {};
170
+ const observer = new MutationObserver(listener);
171
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style'] });
172
+ return () => observer.disconnect();
173
+ },
174
+ });
175
+
176
+ export const getColorSchemeMode = (): ColorSchemeMode => colorSchemeStore.getMode();
177
+ export const getResolvedColorScheme = (): ResolvedColorScheme => colorSchemeStore.getResolved();
178
+ export const setColorSchemeMode = (mode: ColorSchemeMode): void => colorSchemeStore.setMode(mode);
179
+ export const toggleColorScheme = (): void => colorSchemeStore.toggle();
180
+ export const subscribeColorScheme = (listener: () => void): (() => void) =>
181
+ colorSchemeStore.subscribe(listener);
182
+ /** Apply the saved scheme on app boot, before React mounts (no flash), and start
183
+ * following the OS preference while no explicit choice is stored. */
184
+ export const initColorScheme = (): void => colorSchemeStore.init();
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Color-scheme core (framework-free) — the localStorage/document store behind the
3
+ * app-wide light/dark toggle. `initColorScheme()` applies the saved (or OS) scheme
4
+ * on app boot (before React mounts → no flash); the React control + hook live in
5
+ * `cursedbelt/react` (`ThemeToggle`, `useColorScheme`).
6
+ */
7
+ export {
8
+ applyColorSchemeToDocument,
9
+ COLOR_SCHEME_STORAGE_KEY,
10
+ type ColorSchemeMode,
11
+ type ColorSchemeStorage,
12
+ type ColorSchemeStore,
13
+ type ColorSchemeStoreOptions,
14
+ colorSchemeStore,
15
+ createColorSchemeStore,
16
+ getColorSchemeMode,
17
+ getResolvedColorScheme,
18
+ initColorScheme,
19
+ type ResolvedColorScheme,
20
+ setColorSchemeMode,
21
+ subscribeColorScheme,
22
+ toggleColorScheme,
23
+ } from './colorSchemeStore';
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Which engine owns which storage domain (01 §2.3/§5).
3
+ *
4
+ * Lives in `core/` because BOTH tiers need it and neither may import the other's
5
+ * graph: the server's CDC bridge fans a domain's writes to that engine's
6
+ * instances, and the client's DevStudio needs the domains an app can snapshot.
7
+ * A second copy on the client would be a map that drifts the day a domain moves
8
+ * — so there is one, here, with no dependency beyond cwip's domain names.
9
+ *
10
+ * `search` is knowledge's derived index and `staging` is a dev-time sandbox, so
11
+ * neither gets an engine of its own.
12
+ */
13
+ import type { DomainName } from 'cwip/domains';
14
+
15
+ export const DOMAIN_OWNER_ENGINE: Readonly<Record<DomainName, string>> = Object.freeze({
16
+ knowledge: 'knowledge',
17
+ media: 'asset',
18
+ search: 'knowledge',
19
+ system: 'system',
20
+ staging: 'dev',
21
+ });
22
+
23
+ /**
24
+ * The domains a set of active engines owns, in DDL order. `staging` is excluded:
25
+ * it is never part of an exported app's storage map (V5 refuses it upstream).
26
+ */
27
+ export const domainsForEngines = (engines: readonly string[]): DomainName[] => {
28
+ const active = new Set(engines);
29
+ return (Object.keys(DOMAIN_OWNER_ENGINE) as DomainName[]).filter(
30
+ (domain) => domain !== 'staging' && active.has(DOMAIN_OWNER_ENGINE[domain]),
31
+ );
32
+ };
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { base64ToBytes, downloadBlob } from './download';
3
+
4
+ describe('base64ToBytes', () => {
5
+ test('decodes base64 to the original bytes', () => {
6
+ const bytes = base64ToBytes(btoa('SQLite'));
7
+ expect(new TextDecoder().decode(bytes)).toBe('SQLite');
8
+ });
9
+ });
10
+
11
+ describe('downloadBlob', () => {
12
+ test('creates then removes a download anchor (happy-dom)', () => {
13
+ let created: HTMLAnchorElement | null = null;
14
+ const origCreate = URL.createObjectURL;
15
+ const origRevoke = URL.revokeObjectURL;
16
+ let revoked = '';
17
+ URL.createObjectURL = () => 'blob:mock';
18
+ URL.revokeObjectURL = (u: string) => {
19
+ revoked = u;
20
+ };
21
+ const origClick = HTMLAnchorElement.prototype.click;
22
+ HTMLAnchorElement.prototype.click = function click(this: HTMLAnchorElement) {
23
+ created = this;
24
+ };
25
+ try {
26
+ downloadBlob('id,body\n1,a', 'notes.csv', 'text/csv');
27
+ expect(created).not.toBeNull();
28
+ expect((created as unknown as HTMLAnchorElement).download).toBe('notes.csv');
29
+ // Anchor is removed synchronously; the object URL revokes on the next tick.
30
+ expect(document.querySelector('a[download]')).toBeNull();
31
+ } finally {
32
+ URL.createObjectURL = origCreate;
33
+ URL.revokeObjectURL = origRevoke;
34
+ HTMLAnchorElement.prototype.click = origClick;
35
+ void revoked;
36
+ }
37
+ });
38
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Trigger a browser file download from in-memory data — the canonical client-side
3
+ * "save this blob" primitive (there was no shared one; call sites hand-rolled
4
+ * `createObjectURL` + `<a download>` + revoke). DOM-only: it no-ops off the browser
5
+ * so importing modules stay server-safe.
6
+ *
7
+ * ```ts
8
+ * downloadBlob(csvText, 'notes.csv', 'text/csv');
9
+ * downloadBlob(sqliteBytes, 'snapshot.sqlite', 'application/x-sqlite3');
10
+ * ```
11
+ */
12
+ export function downloadBlob(data: BlobPart, filename: string, mime: string): void {
13
+ if (typeof document === 'undefined' || typeof URL.createObjectURL !== 'function') return;
14
+ const url = URL.createObjectURL(new Blob([data], { type: mime }));
15
+ try {
16
+ const a = document.createElement('a');
17
+ a.href = url;
18
+ a.download = filename;
19
+ a.rel = 'noopener';
20
+ document.body.appendChild(a);
21
+ a.click();
22
+ a.remove();
23
+ } finally {
24
+ // Revoke on the next tick so the click's navigation has grabbed the URL first.
25
+ setTimeout(() => URL.revokeObjectURL(url), 0);
26
+ }
27
+ }
28
+
29
+ /** Decode a base64 payload (e.g. an exported `.sqlite` snapshot) to bytes for {@link downloadBlob}. */
30
+ export function base64ToBytes(base64: string): Uint8Array {
31
+ const bin = atob(base64);
32
+ const bytes = new Uint8Array(bin.length);
33
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
34
+ return bytes;
35
+ }
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { createCcEventBus, MAX_EVENT_DEPTH } from './ccEventBus';
3
+
4
+ describe('CCEventBus', () => {
5
+ it('delivers to every subscriber of a type and to nobody else', async () => {
6
+ const bus = createCcEventBus();
7
+ const saved: string[] = [];
8
+ const locked: string[] = [];
9
+ bus.on('NOTE_SAVED', (event) => saved.push(event.type));
10
+ bus.on('NOTE_SAVED', (event) => saved.push(`${event.type}:second`));
11
+ bus.on('VAULT_LOCKED', (event) => locked.push(event.type));
12
+
13
+ bus.emit({ type: 'NOTE_SAVED' });
14
+ await bus.settled();
15
+
16
+ expect(saved).toEqual(['NOTE_SAVED', 'NOTE_SAVED:second']);
17
+ expect(locked).toEqual([]);
18
+ });
19
+
20
+ it('unsubscribes cleanly', async () => {
21
+ const bus = createCcEventBus();
22
+ const seen: number[] = [];
23
+ const off = bus.on('APP_DEPLOYED', () => seen.push(1));
24
+ bus.emit({ type: 'APP_DEPLOYED' });
25
+ await bus.settled();
26
+ off();
27
+ bus.emit({ type: 'APP_DEPLOYED' });
28
+ await bus.settled();
29
+ expect(seen).toEqual([1]);
30
+ });
31
+
32
+ it('keeps a throwing subscriber from stopping the rest of the chain', async () => {
33
+ const bus = createCcEventBus();
34
+ const seen: string[] = [];
35
+ bus.on('NOTE_SAVED', () => {
36
+ throw new Error('subscriber blew up');
37
+ });
38
+ bus.on('NOTE_SAVED', () => seen.push('second ran'));
39
+ bus.emit({ type: 'NOTE_SAVED' });
40
+ await bus.settled();
41
+ expect(seen).toEqual(['second ran']);
42
+ });
43
+
44
+ it('terminates a chain past the depth limit and names the whole chain', async () => {
45
+ const diagnostics: string[][] = [];
46
+ const bus = createCcEventBus({ onLoopDiagnostic: (d) => diagnostics.push(d.chain) });
47
+ const chain = ['A', 'B', 'C', 'D', 'E', 'F'];
48
+ let handled = 0;
49
+
50
+ chain.forEach((type, index) => {
51
+ const next = chain[(index + 1) % chain.length] as string;
52
+ bus.on(type, (event) => {
53
+ handled += 1;
54
+ bus.emit({ type: next, causationId: event.causationId, depth: event.depth + 1 });
55
+ });
56
+ });
57
+
58
+ bus.emit({ type: 'A' });
59
+ await bus.settled();
60
+
61
+ expect(diagnostics).toHaveLength(1);
62
+ expect(diagnostics[0]?.length).toBe(MAX_EVENT_DEPTH + 2); // the chain up to and including the terminated emission
63
+ expect(handled).toBeLessThanOrEqual(MAX_EVENT_DEPTH + 1);
64
+ });
65
+
66
+ it('lets an independent chain run to its own depth — the guard is per causation', async () => {
67
+ const bus = createCcEventBus();
68
+ bus.on('PING', (event) => {
69
+ if (event.depth < 3) bus.emit({ type: 'PING', causationId: event.causationId, depth: event.depth + 1 });
70
+ });
71
+ bus.emit({ type: 'PING', causationId: 'chain-1' });
72
+ bus.emit({ type: 'PING', causationId: 'chain-2' });
73
+ await bus.settled();
74
+ expect(bus.diagnostics()).toEqual([]);
75
+ });
76
+
77
+ it('schedules handlers instead of re-entering the dispatcher', async () => {
78
+ const bus = createCcEventBus();
79
+ const order: string[] = [];
80
+ bus.on('FIRST', () => {
81
+ order.push('first-start');
82
+ bus.emit({ type: 'SECOND' });
83
+ order.push('first-end');
84
+ });
85
+ bus.on('SECOND', () => order.push('second'));
86
+ bus.emit({ type: 'FIRST' });
87
+ await bus.settled();
88
+ expect(order).toEqual(['first-start', 'first-end', 'second']);
89
+ });
90
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The global event bus (03 §6.4) — lifecycle notifications only, never data
3
+ * payloads (those ride the port hub).
4
+ *
5
+ * The depth guard is the reason this is a primitive rather than an EventEmitter:
6
+ * handlers emit, and a handler chain that loops — `NOTE_SAVED` → indexer →
7
+ * dashboard → audit interceptor → `NOTE_SAVED` — freezes the main thread with no
8
+ * error and no stack that names the cycle. Each emission inherits
9
+ * `depth = parent.depth + 1` via `causationId`; past `MAX_EVENT_DEPTH` the bus
10
+ * terminates the chain and raises an `EventLoopDiagnostic` that names the whole
11
+ * chain, so the loop is a report instead of a hang.
12
+ *
13
+ * Handlers are async-scheduled, never re-entrant: emitting from inside a handler
14
+ * queues, so a synchronous handler cannot re-enter the dispatcher mid-iteration.
15
+ */
16
+ export interface CCEvent {
17
+ type: string;
18
+ tenantId: string;
19
+ causationId: string;
20
+ depth: number;
21
+ payload: Record<string, unknown>;
22
+ }
23
+
24
+ export interface EventLoopDiagnostic {
25
+ chain: string[];
26
+ terminatedAt: string;
27
+ causationId: string;
28
+ }
29
+
30
+ export type Unsubscribe = () => void;
31
+
32
+ /** Past this depth a chain is a loop, not a workflow (03 §6.4). */
33
+ export const MAX_EVENT_DEPTH = 5;
34
+
35
+ export interface CCEventBusOptions {
36
+ onLoopDiagnostic?: (diagnostic: EventLoopDiagnostic) => void;
37
+ maxDepth?: number;
38
+ }
39
+
40
+ export interface CCEventBus {
41
+ emit(event: Partial<CCEvent> & { type: string }): void;
42
+ on(type: string, handler: (event: CCEvent) => void): Unsubscribe;
43
+ /** Resolves once the queue drains — the probe's "is the main thread still responsive?" hook. */
44
+ settled(): Promise<void>;
45
+ diagnostics(): readonly EventLoopDiagnostic[];
46
+ }
47
+
48
+ export function createCcEventBus(options: CCEventBusOptions = {}): CCEventBus {
49
+ const maxDepth = options.maxDepth ?? MAX_EVENT_DEPTH;
50
+ const handlers = new Map<string, ((event: CCEvent) => void)[]>();
51
+ const diagnostics: EventLoopDiagnostic[] = [];
52
+ const chains = new Map<string, string[]>();
53
+ const queue: CCEvent[] = [];
54
+ let draining = false;
55
+ let drained: Promise<void> = Promise.resolve();
56
+ let resolveDrained: (() => void) | null = null;
57
+
58
+ const drain = async (): Promise<void> => {
59
+ draining = true;
60
+ while (queue.length > 0) {
61
+ const event = queue.shift() as CCEvent;
62
+ for (const handler of handlers.get(event.type) ?? []) {
63
+ try {
64
+ handler(event);
65
+ } catch {
66
+ // A throwing subscriber must not stop the rest of the chain.
67
+ }
68
+ }
69
+ await Promise.resolve(); // yield: handlers are scheduled, never re-entrant
70
+ }
71
+ draining = false;
72
+ resolveDrained?.();
73
+ resolveDrained = null;
74
+ };
75
+
76
+ return {
77
+ emit(partial) {
78
+ const causationId = partial.causationId ?? `cause_${chains.size + 1}_${partial.type}`;
79
+ const depth = partial.depth ?? 0;
80
+ const chain = [...(chains.get(causationId) ?? []), partial.type];
81
+ chains.set(causationId, chain);
82
+
83
+ if (depth > maxDepth) {
84
+ const diagnostic: EventLoopDiagnostic = { chain, terminatedAt: partial.type, causationId };
85
+ diagnostics.push(diagnostic);
86
+ options.onLoopDiagnostic?.(diagnostic);
87
+ return; // chain terminated — the guard, not a crash
88
+ }
89
+
90
+ queue.push({
91
+ type: partial.type,
92
+ tenantId: partial.tenantId ?? 'tenant-a',
93
+ causationId,
94
+ depth,
95
+ payload: partial.payload ?? {},
96
+ });
97
+
98
+ if (!draining) {
99
+ drained = new Promise<void>((resolve) => {
100
+ resolveDrained = resolve;
101
+ });
102
+ void drain();
103
+ }
104
+ },
105
+
106
+ on(type, handler) {
107
+ const list = handlers.get(type) ?? [];
108
+ list.push(handler);
109
+ handlers.set(type, list);
110
+ return () => {
111
+ const current = handlers.get(type) ?? [];
112
+ const index = current.indexOf(handler);
113
+ if (index >= 0) current.splice(index, 1);
114
+ };
115
+ },
116
+
117
+ settled: () => (draining ? drained : Promise.resolve()),
118
+ diagnostics: () => diagnostics,
119
+ };
120
+ }