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.
- package/LICENSE +15 -0
- package/dist/client/wsWire.d.ts +49 -0
- package/dist/client/wsWire.js +57 -0
- package/dist/config/appConfig.d.ts +361 -0
- package/dist/config/appConfig.js +308 -0
- package/dist/core/activity/index.d.ts +8 -0
- package/dist/core/activity/index.js +7 -0
- package/dist/core/activity/model.d.ts +115 -0
- package/dist/core/activity/model.js +63 -0
- package/dist/core/analytics/index.d.ts +18 -0
- package/dist/core/analytics/index.js +18 -0
- package/dist/core/analytics/range.d.ts +186 -0
- package/dist/core/analytics/range.js +271 -0
- package/dist/core/analytics/schedule.d.ts +301 -0
- package/dist/core/analytics/schedule.js +372 -0
- package/dist/core/category/index.d.ts +3 -0
- package/dist/core/category/index.js +6 -0
- package/dist/core/category/memoryStore.d.ts +21 -0
- package/dist/core/category/memoryStore.js +97 -0
- package/dist/core/category/tree.d.ts +26 -0
- package/dist/core/category/tree.js +113 -0
- package/dist/core/category/types.d.ts +48 -0
- package/dist/core/category/types.js +8 -0
- package/dist/core/color.d.ts +14 -0
- package/dist/core/color.js +32 -0
- package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
- package/dist/core/colorScheme/colorSchemeStore.js +123 -0
- package/dist/core/colorScheme/index.d.ts +7 -0
- package/dist/core/colorScheme/index.js +7 -0
- package/dist/core/domainOwners.d.ts +19 -0
- package/dist/core/domainOwners.js +15 -0
- package/dist/core/download.d.ts +14 -0
- package/dist/core/download.js +37 -0
- package/dist/core/events/ccEventBus.d.ts +44 -0
- package/dist/core/events/ccEventBus.js +70 -0
- package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
- package/dist/core/file-tree/fileTreeModel.js +312 -0
- package/dist/core/folder-tree/actions.d.ts +84 -0
- package/dist/core/folder-tree/actions.js +119 -0
- package/dist/core/folder-tree/index.d.ts +3 -0
- package/dist/core/folder-tree/index.js +20 -0
- package/dist/core/folder-tree/path.d.ts +76 -0
- package/dist/core/folder-tree/path.js +146 -0
- package/dist/core/folder-tree/tree.d.ts +86 -0
- package/dist/core/folder-tree/tree.js +184 -0
- package/dist/core/layout/index.d.ts +1 -0
- package/dist/core/layout/index.js +7 -0
- package/dist/core/master-lock/index.d.ts +14 -0
- package/dist/core/master-lock/index.js +14 -0
- package/dist/core/master-lock/kdf.d.ts +63 -0
- package/dist/core/master-lock/kdf.js +117 -0
- package/dist/core/master-lock/policy.d.ts +32 -0
- package/dist/core/master-lock/policy.js +52 -0
- package/dist/core/master-lock/presence.d.ts +52 -0
- package/dist/core/master-lock/presence.js +61 -0
- package/dist/core/master-lock/wire.d.ts +139 -0
- package/dist/core/master-lock/wire.js +81 -0
- package/dist/core/media/bgRemoval.d.ts +27 -0
- package/dist/core/media/bgRemoval.js +101 -0
- package/dist/core/media/ffmpeg.d.ts +57 -0
- package/dist/core/media/ffmpeg.js +126 -0
- package/dist/core/media/format.d.ts +20 -0
- package/dist/core/media/format.js +54 -0
- package/dist/core/media/index.d.ts +12 -0
- package/dist/core/media/index.js +18 -0
- package/dist/core/media/mediaItem.d.ts +15 -0
- package/dist/core/media/mediaItem.js +34 -0
- package/dist/core/media/renderPlan.d.ts +63 -0
- package/dist/core/media/renderPlan.js +232 -0
- package/dist/core/media/segment.d.ts +79 -0
- package/dist/core/media/segment.js +247 -0
- package/dist/core/media/size.d.ts +1 -0
- package/dist/core/media/size.js +3 -0
- package/dist/core/media/trim.d.ts +21 -0
- package/dist/core/media/trim.js +40 -0
- package/dist/core/media/types.d.ts +148 -0
- package/dist/core/media/types.js +11 -0
- package/dist/core/media/uploadLimits.d.ts +7 -0
- package/dist/core/media/uploadLimits.js +16 -0
- package/dist/core/media/validate.d.ts +9 -0
- package/dist/core/media/validate.js +121 -0
- package/dist/core/media/videoCuts.d.ts +152 -0
- package/dist/core/media/videoCuts.js +284 -0
- package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
- package/dist/core/navigation/breadcrumbCompat.js +21 -0
- package/dist/core/navigation/coverageTest.d.ts +24 -0
- package/dist/core/navigation/coverageTest.js +48 -0
- package/dist/core/navigation/index.d.ts +6 -0
- package/dist/core/navigation/index.js +10 -0
- package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
- package/dist/core/navigation/migrateNavPreferences.js +63 -0
- package/dist/core/navigation/navigationModel.d.ts +165 -0
- package/dist/core/navigation/navigationModel.js +14 -0
- package/dist/core/navigation/navigationSelectors.d.ts +63 -0
- package/dist/core/navigation/navigationSelectors.js +167 -0
- package/dist/core/navigation/searchCatalogue.d.ts +23 -0
- package/dist/core/navigation/searchCatalogue.js +76 -0
- package/dist/core/net/reconnectSchedule.d.ts +46 -0
- package/dist/core/net/reconnectSchedule.js +38 -0
- package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
- package/dist/core/palette/ShortcutRegistry.js +52 -0
- package/dist/core/palette/fuzzyRank.d.ts +18 -0
- package/dist/core/palette/fuzzyRank.js +73 -0
- package/dist/core/palette/hotkeys.d.ts +28 -0
- package/dist/core/palette/hotkeys.js +118 -0
- package/dist/core/palette/index.d.ts +3 -0
- package/dist/core/palette/index.js +8 -0
- package/dist/core/schema/projection.d.ts +84 -0
- package/dist/core/schema/projection.js +184 -0
- package/dist/core/session/claims.d.ts +145 -0
- package/dist/core/session/claims.js +157 -0
- package/dist/core/sharing/index.d.ts +5 -0
- package/dist/core/sharing/index.js +4 -0
- package/dist/core/sharing/model.d.ts +195 -0
- package/dist/core/sharing/model.js +150 -0
- package/dist/core/site/siteCopy.d.ts +92 -0
- package/dist/core/site/siteCopy.js +91 -0
- package/dist/core/slots.d.ts +22 -0
- package/dist/core/slots.js +31 -0
- package/dist/core/styles/constructableSheet.d.ts +51 -0
- package/dist/core/styles/constructableSheet.js +80 -0
- package/dist/core/tokens/registry.d.ts +119 -0
- package/dist/core/tokens/registry.js +447 -0
- package/dist/core/uiScale/index.d.ts +8 -0
- package/dist/core/uiScale/index.js +8 -0
- package/dist/core/uiScale/scale.d.ts +36 -0
- package/dist/core/uiScale/scale.js +47 -0
- package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
- package/dist/core/uiScale/uiScaleStore.js +63 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
- package/dist/core/wire/index.d.ts +17 -0
- package/dist/core/wire/index.js +17 -0
- package/dist/core/wire/ingress.d.ts +44 -0
- package/dist/core/wire/ingress.js +134 -0
- package/dist/core/wire/marshalWorker.d.ts +70 -0
- package/dist/core/wire/marshalWorker.js +273 -0
- package/dist/core/wire/marshalling.d.ts +122 -0
- package/dist/core/wire/marshalling.js +126 -0
- package/dist/core/wire/wireHub.d.ts +80 -0
- package/dist/core/wire/wireHub.js +116 -0
- package/dist/ctgr/base64url.d.ts +8 -0
- package/dist/ctgr/base64url.js +39 -0
- package/dist/ctgr/chunkAccumulator.d.ts +214 -0
- package/dist/ctgr/chunkAccumulator.js +307 -0
- package/dist/ctgr/chunkStore.d.ts +24 -0
- package/dist/ctgr/chunkStore.js +125 -0
- package/dist/ctgr/codec.d.ts +44 -0
- package/dist/ctgr/codec.js +153 -0
- package/dist/ctgr/compress.d.ts +2 -0
- package/dist/ctgr/compress.js +20 -0
- package/dist/ctgr/crc32.d.ts +2 -0
- package/dist/ctgr/crc32.js +23 -0
- package/dist/ctgr/digest.d.ts +1 -0
- package/dist/ctgr/digest.js +15 -0
- package/dist/ctgr/index.d.ts +9 -0
- package/dist/ctgr/index.js +17 -0
- package/dist/ctgr/types.d.ts +64 -0
- package/dist/ctgr/types.js +21 -0
- package/dist/ctgr/v0compat.d.ts +27 -0
- package/dist/ctgr/v0compat.js +128 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/shared/errors.d.ts +101 -0
- package/dist/shared/errors.js +99 -0
- package/dist/testing/bytes.d.ts +32 -0
- package/dist/testing/bytes.js +33 -0
- package/dist/testing/domInspect.d.ts +59 -0
- package/dist/testing/domInspect.js +115 -0
- package/dist/testing/fakeFetch.d.ts +23 -0
- package/dist/testing/fakeFetch.js +21 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +11 -0
- package/package.json +373 -0
- package/src/client/wsWire.ts +83 -0
- package/src/config/appConfig.spec.ts +158 -0
- package/src/config/appConfig.ts +372 -0
- package/src/core/activity/index.ts +21 -0
- package/src/core/activity/model.ts +160 -0
- package/src/core/analytics/index.ts +18 -0
- package/src/core/analytics/range.spec.ts +280 -0
- package/src/core/analytics/range.ts +383 -0
- package/src/core/analytics/schedule.spec.ts +311 -0
- package/src/core/analytics/schedule.ts +624 -0
- package/src/core/category/category.spec.ts +144 -0
- package/src/core/category/index.ts +19 -0
- package/src/core/category/memoryStore.ts +104 -0
- package/src/core/category/tree.ts +118 -0
- package/src/core/category/types.ts +59 -0
- package/src/core/color.spec.ts +35 -0
- package/src/core/color.ts +35 -0
- package/src/core/colorScheme/colorScheme.spec.ts +127 -0
- package/src/core/colorScheme/colorSchemeStore.ts +184 -0
- package/src/core/colorScheme/index.ts +23 -0
- package/src/core/domainOwners.ts +32 -0
- package/src/core/download.spec.ts +38 -0
- package/src/core/download.ts +35 -0
- package/src/core/events/ccEventBus.spec.ts +90 -0
- package/src/core/events/ccEventBus.ts +120 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
- package/src/core/file-tree/fileTreeModel.ts +511 -0
- package/src/core/folder-tree/actions.spec.ts +103 -0
- package/src/core/folder-tree/actions.ts +153 -0
- package/src/core/folder-tree/index.ts +46 -0
- package/src/core/folder-tree/path.spec.ts +113 -0
- package/src/core/folder-tree/path.ts +148 -0
- package/src/core/folder-tree/tree.spec.ts +157 -0
- package/src/core/folder-tree/tree.ts +244 -0
- package/src/core/layout/index.ts +7 -0
- package/src/core/master-lock/index.ts +50 -0
- package/src/core/master-lock/kdf.spec.ts +95 -0
- package/src/core/master-lock/kdf.ts +145 -0
- package/src/core/master-lock/policy.ts +55 -0
- package/src/core/master-lock/presence.spec.ts +73 -0
- package/src/core/master-lock/presence.ts +71 -0
- package/src/core/master-lock/wire.ts +155 -0
- package/src/core/media/bgRemoval.spec.ts +120 -0
- package/src/core/media/bgRemoval.ts +123 -0
- package/src/core/media/ffmpeg.spec.ts +135 -0
- package/src/core/media/ffmpeg.ts +162 -0
- package/src/core/media/format.spec.ts +41 -0
- package/src/core/media/format.ts +58 -0
- package/src/core/media/index.ts +105 -0
- package/src/core/media/mediaItem.spec.ts +46 -0
- package/src/core/media/mediaItem.ts +40 -0
- package/src/core/media/renderPlan.spec.ts +140 -0
- package/src/core/media/renderPlan.ts +318 -0
- package/src/core/media/segment.spec.ts +159 -0
- package/src/core/media/segment.ts +352 -0
- package/src/core/media/size.spec.ts +19 -0
- package/src/core/media/size.ts +3 -0
- package/src/core/media/trim.spec.ts +75 -0
- package/src/core/media/trim.ts +60 -0
- package/src/core/media/types.ts +147 -0
- package/src/core/media/uploadLimits.ts +18 -0
- package/src/core/media/validate.spec.ts +106 -0
- package/src/core/media/validate.ts +131 -0
- package/src/core/media/videoCuts.spec.ts +327 -0
- package/src/core/media/videoCuts.ts +355 -0
- package/src/core/navigation/breadcrumbCompat.ts +44 -0
- package/src/core/navigation/coverageTest.ts +54 -0
- package/src/core/navigation/index.ts +50 -0
- package/src/core/navigation/migrateNavPreferences.ts +119 -0
- package/src/core/navigation/navigation.spec.ts +388 -0
- package/src/core/navigation/navigationModel.ts +185 -0
- package/src/core/navigation/navigationSelectors.ts +206 -0
- package/src/core/navigation/searchCatalogue.ts +89 -0
- package/src/core/net/reconnectSchedule.spec.ts +49 -0
- package/src/core/net/reconnectSchedule.ts +78 -0
- package/src/core/palette/ShortcutRegistry.ts +69 -0
- package/src/core/palette/fuzzyRank.ts +78 -0
- package/src/core/palette/hotkeys.ts +132 -0
- package/src/core/palette/index.ts +22 -0
- package/src/core/palette/palette.spec.ts +160 -0
- package/src/core/schema/projection.spec.ts +122 -0
- package/src/core/schema/projection.ts +225 -0
- package/src/core/session/claims.ts +181 -0
- package/src/core/sharing/index.ts +33 -0
- package/src/core/sharing/model.spec.ts +104 -0
- package/src/core/sharing/model.ts +296 -0
- package/src/core/site/siteCopy.ts +115 -0
- package/src/core/slots.ts +38 -0
- package/src/core/styles/constructableSheet.ts +86 -0
- package/src/core/tokens/registry.spec.ts +160 -0
- package/src/core/tokens/registry.ts +536 -0
- package/src/core/uiScale/index.ts +32 -0
- package/src/core/uiScale/scale.ts +63 -0
- package/src/core/uiScale/uiScale.spec.ts +77 -0
- package/src/core/uiScale/uiScaleStore.ts +100 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
- package/src/core/wire/index.ts +56 -0
- package/src/core/wire/ingress.spec.ts +276 -0
- package/src/core/wire/ingress.ts +182 -0
- package/src/core/wire/marshalWorker.spec.ts +196 -0
- package/src/core/wire/marshalWorker.ts +361 -0
- package/src/core/wire/marshalling.spec.ts +139 -0
- package/src/core/wire/marshalling.ts +238 -0
- package/src/core/wire/wireHub.spec.ts +137 -0
- package/src/core/wire/wireHub.ts +213 -0
- package/src/ctgr/base64url.ts +44 -0
- package/src/ctgr/chunkAccumulator.ts +403 -0
- package/src/ctgr/chunkStore.spec.ts +101 -0
- package/src/ctgr/chunkStore.ts +168 -0
- package/src/ctgr/codec.spec.ts +208 -0
- package/src/ctgr/codec.ts +202 -0
- package/src/ctgr/compress.ts +21 -0
- package/src/ctgr/crc32.ts +23 -0
- package/src/ctgr/digest.ts +14 -0
- package/src/ctgr/index.ts +39 -0
- package/src/ctgr/types.ts +80 -0
- package/src/ctgr/v0compat.spec.ts +73 -0
- package/src/ctgr/v0compat.ts +156 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/index.ts +25 -0
- package/src/leafSubpathsImportNothing.spec.ts +287 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/shared/errors.ts +153 -0
- package/src/skippedTestsAreAnswered.spec.ts +221 -0
- package/src/testTmpRoot.spec.ts +102 -0
- package/src/testing/bytes.ts +38 -0
- package/src/testing/domInspect.spec.ts +79 -0
- package/src/testing/domInspect.ts +127 -0
- package/src/testing/fakeFetch.ts +26 -0
- package/src/testing/index.ts +11 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The closed token set — the compile-time vocabulary every wire is drawn from
|
|
3
|
+
* (04 §2 V3). A `PortSpec` may only reference a `TokenDef` obtained from this
|
|
4
|
+
* registry; anything hand-declared fails V3 `token-not-registered`, which is
|
|
5
|
+
* what makes "compatible" a decidable property instead of a naming convention.
|
|
6
|
+
*
|
|
7
|
+
* Compatibility is **lattice membership**, not string equality: a wire connects
|
|
8
|
+
* iff `produced ⊑ consumed` under the reflexive-transitive closure of
|
|
9
|
+
* `subtypeOf`. String equality is retired — it forced every producer of a
|
|
10
|
+
* narrower thing (a photo feed) to lie about its type to reach a consumer of
|
|
11
|
+
* the wider thing (a media grid).
|
|
12
|
+
*
|
|
13
|
+
* Staging plugins extend the set at design time through `x-<pluginId>:*` ids
|
|
14
|
+
* (01 §5.2). Three laws keep the extension mechanism from corrupting the core
|
|
15
|
+
* vocabulary, all enforced here and reported by V3:
|
|
16
|
+
* - namespace: an extension id must be `x-<pluginId>:<name>`;
|
|
17
|
+
* - collision: an extension may never redeclare a base id;
|
|
18
|
+
* - inversion: a core token may never declare an extension as its supertype
|
|
19
|
+
* (the core would stop compiling the moment the plugin is deleted).
|
|
20
|
+
*/
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
|
|
23
|
+
export type TokenMode = 'value' | 'signal';
|
|
24
|
+
|
|
25
|
+
/** JSON-Schema description of a token payload — the unit of structural comparison (J4-D). */
|
|
26
|
+
export type SchemaShape = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
export interface TokenDeprecation {
|
|
29
|
+
replacedBy: string;
|
|
30
|
+
/** Free-form marker of when the deprecation started; surfaced in the V3 warning. */
|
|
31
|
+
since: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TokenDef<S extends z.ZodType = z.ZodType> {
|
|
35
|
+
/** `<namespace>:<name>` — namespace is an engine id or `x-<pluginId>` for staging. */
|
|
36
|
+
id: string;
|
|
37
|
+
mode: TokenMode;
|
|
38
|
+
schema: S;
|
|
39
|
+
/** Parent token id; the single lattice edge. Absent ⇒ a lattice root. */
|
|
40
|
+
subtypeOf?: string;
|
|
41
|
+
/** Canonical payload injected by the V7.4 wire smoke probe. Must satisfy `schema`. */
|
|
42
|
+
sample: z.infer<S>;
|
|
43
|
+
/** True when this token receives third-party traffic — triggers the V3 marshaller gates. */
|
|
44
|
+
external?: boolean;
|
|
45
|
+
deprecated?: TokenDeprecation;
|
|
46
|
+
description: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 02 §7.1 — the only entry point for external payloads.
|
|
51
|
+
*
|
|
52
|
+
* Both symbols are DEFINED in `core/wire/marshalling.ts` beside the runtime that
|
|
53
|
+
* enforces them, and re-exported here because every existing importer (V3,
|
|
54
|
+
* `verify/types.ts`, the fixtures) reaches for them through the token registry.
|
|
55
|
+
* They used to be a second, divergent declaration of the same names — see the
|
|
56
|
+
* unification note on `CCWireMarshaller` (05 §B1).
|
|
57
|
+
*/
|
|
58
|
+
import type { CCWireMarshaller } from '../wire/marshalling';
|
|
59
|
+
|
|
60
|
+
export {
|
|
61
|
+
type CCWireMarshaller,
|
|
62
|
+
MARSHALLER_WORKER_OFFLOAD_BYTES,
|
|
63
|
+
requiresWorkerOffload,
|
|
64
|
+
} from '../wire/marshalling';
|
|
65
|
+
|
|
66
|
+
export type TokenRegistryIssueCode =
|
|
67
|
+
| 'token-collision'
|
|
68
|
+
| 'token-namespace-violation'
|
|
69
|
+
| 'subtype-inversion'
|
|
70
|
+
| 'token-supertype-missing'
|
|
71
|
+
| 'token-subtype-cycle'
|
|
72
|
+
| 'token-sample-invalid';
|
|
73
|
+
|
|
74
|
+
export interface TokenRegistryIssue {
|
|
75
|
+
code: TokenRegistryIssueCode;
|
|
76
|
+
tokenId: string;
|
|
77
|
+
message: string;
|
|
78
|
+
/** Set for harvested extensions so V3 can point the diagnostic at the source file. */
|
|
79
|
+
path?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const TOKEN_ID_RE = /^[a-z][a-z0-9-]*:[a-z0-9-]+$/;
|
|
83
|
+
const EXTENSION_ID_RE = /^x-[a-z0-9-]+:[a-z0-9-]+$/;
|
|
84
|
+
|
|
85
|
+
export const isExtensionTokenId = (id: string): boolean => id.startsWith('x-');
|
|
86
|
+
|
|
87
|
+
/** `x-habits:mark` ⇒ `habits`; `media:videos` ⇒ `media`. */
|
|
88
|
+
export const namespaceOf = (id: string): string => id.slice(0, id.indexOf(':'));
|
|
89
|
+
|
|
90
|
+
/** `x-habits:mark` ⇒ `plugin-habits`. */
|
|
91
|
+
export const stagingPluginOf = (id: string): string => `plugin-${namespaceOf(id).slice('x-'.length)}`;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Normalized structural description of a token payload. Two schemas that were
|
|
95
|
+
* authored independently in two repos still compare, which is the whole point of
|
|
96
|
+
* the J4-D fallback: identity is the fast path, structure is the truth.
|
|
97
|
+
*/
|
|
98
|
+
export function describeSchema(schema: z.ZodType): SchemaShape {
|
|
99
|
+
const described = z.toJSONSchema(schema, { io: 'output', unrepresentable: 'any' }) as SchemaShape;
|
|
100
|
+
const { $schema: _ignored, ...rest } = described;
|
|
101
|
+
return rest;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type SchemaComparison = 'identical' | 'backward-compatible-extension' | 'divergent';
|
|
105
|
+
|
|
106
|
+
const stableStringify = (value: unknown): string => {
|
|
107
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
|
|
108
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
109
|
+
const entries = Object.entries(value as Record<string, unknown>).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
110
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(',')}}`;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compare a producer's payload shape against the consumer's canonical shape.
|
|
115
|
+
*
|
|
116
|
+
* `backward-compatible-extension` means the producer satisfies every requirement
|
|
117
|
+
* the consumer states AND everything it adds is optional — a consumer written
|
|
118
|
+
* against the older shape still validates every payload the newer producer
|
|
119
|
+
* emits. Anything else is drift: same token id, different contract, silent
|
|
120
|
+
* runtime breakage (the `media:video-ref` `{id}` vs `{id,name}` class).
|
|
121
|
+
*/
|
|
122
|
+
export function compareSchemaShapes(producer: SchemaShape, consumer: SchemaShape): SchemaComparison {
|
|
123
|
+
return compareNodes(producer, consumer);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The comparison recurses, because the interesting skew is never at the top
|
|
128
|
+
* level: a producer that added an optional `caption` to each item of
|
|
129
|
+
* `{ items: [...] }` differs only three levels down, and a shallow compare would
|
|
130
|
+
* call that drift and block a build for no reason.
|
|
131
|
+
*/
|
|
132
|
+
function compareNodes(producer: unknown, consumer: unknown): SchemaComparison {
|
|
133
|
+
if (stableStringify(producer) === stableStringify(consumer)) return 'identical';
|
|
134
|
+
if (typeof producer !== 'object' || producer === null || typeof consumer !== 'object' || consumer === null)
|
|
135
|
+
return 'divergent';
|
|
136
|
+
|
|
137
|
+
const producerNode = producer as Record<string, unknown>;
|
|
138
|
+
const consumerNode = consumer as Record<string, unknown>;
|
|
139
|
+
|
|
140
|
+
if (producerNode.type === 'array' && consumerNode.type === 'array') return compareNodes(producerNode.items, consumerNode.items);
|
|
141
|
+
|
|
142
|
+
const producerProps = (producerNode.properties ?? null) as Record<string, unknown> | null;
|
|
143
|
+
const consumerProps = (consumerNode.properties ?? null) as Record<string, unknown> | null;
|
|
144
|
+
if (producerNode.type !== 'object' || consumerNode.type !== 'object' || !producerProps || !consumerProps) return 'divergent';
|
|
145
|
+
|
|
146
|
+
const producerRequired = new Set((producerNode.required as string[] | undefined) ?? []);
|
|
147
|
+
const consumerRequired = new Set((consumerNode.required as string[] | undefined) ?? []);
|
|
148
|
+
|
|
149
|
+
const widen = (current: SchemaComparison, next: SchemaComparison): SchemaComparison => {
|
|
150
|
+
if (current === 'divergent' || next === 'divergent') return 'divergent';
|
|
151
|
+
if (current === 'backward-compatible-extension' || next === 'backward-compatible-extension')
|
|
152
|
+
return 'backward-compatible-extension';
|
|
153
|
+
return 'identical';
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
let verdict: SchemaComparison = 'identical';
|
|
157
|
+
|
|
158
|
+
for (const [key, consumerProp] of Object.entries(consumerProps)) {
|
|
159
|
+
const producerProp = producerProps[key];
|
|
160
|
+
if (producerProp === undefined) return 'divergent'; // a field the consumer needs and the producer never emits
|
|
161
|
+
if (consumerRequired.has(key) && !producerRequired.has(key)) return 'divergent'; // may arrive absent
|
|
162
|
+
verdict = widen(verdict, compareNodes(producerProp, consumerProp));
|
|
163
|
+
if (verdict === 'divergent') return 'divergent';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const key of Object.keys(producerProps)) {
|
|
167
|
+
if (key in consumerProps) continue;
|
|
168
|
+
// A required extra field is NOT backward compatible: a consumer that echoes
|
|
169
|
+
// the payload back through the same token would fail the producer's schema.
|
|
170
|
+
if (producerRequired.has(key)) return 'divergent';
|
|
171
|
+
verdict = widen(verdict, 'backward-compatible-extension');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return verdict;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface ConnectResult {
|
|
178
|
+
ok: boolean;
|
|
179
|
+
reason?: 'unknown-produced' | 'unknown-consumed' | 'mode-mismatch' | 'not-a-subtype';
|
|
180
|
+
message?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export class TokenRegistry {
|
|
184
|
+
private readonly tokens = new Map<string, TokenDef>();
|
|
185
|
+
private readonly marshallers = new Map<string, CCWireMarshaller>();
|
|
186
|
+
private readonly shapes = new Map<string, SchemaShape>();
|
|
187
|
+
|
|
188
|
+
constructor(base: readonly TokenDef[] = []) {
|
|
189
|
+
const issues = this.registerAll(base, { origin: 'base' });
|
|
190
|
+
if (issues.length > 0) {
|
|
191
|
+
// Base tokens are authored code, not user data — a broken base registry is a
|
|
192
|
+
// programming error that must never reach a diagnostic list.
|
|
193
|
+
throw new Error(`Invalid base token registry: ${issues.map((i) => `${i.code} (${i.tokenId})`).join(', ')}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Register harvested staging extensions. Returns issues rather than throwing:
|
|
199
|
+
* these ids come from files on disk, so they are data and belong in the
|
|
200
|
+
* diagnostic list (V3), not in a crash.
|
|
201
|
+
*/
|
|
202
|
+
registerAll(defs: readonly TokenDef[], opts: { origin: 'base' | 'staging'; path?: string } = { origin: 'staging' }): TokenRegistryIssue[] {
|
|
203
|
+
const issues: TokenRegistryIssue[] = [];
|
|
204
|
+
const accepted: TokenDef[] = [];
|
|
205
|
+
|
|
206
|
+
for (const def of defs) {
|
|
207
|
+
const path = opts.path;
|
|
208
|
+
// Collision is checked BEFORE the namespace laws: an extension that
|
|
209
|
+
// redeclares `media:videos` is a collision, and saying "bad namespace"
|
|
210
|
+
// would send the author to fix the wrong thing.
|
|
211
|
+
if (this.tokens.has(def.id)) {
|
|
212
|
+
issues.push({
|
|
213
|
+
code: 'token-collision',
|
|
214
|
+
tokenId: def.id,
|
|
215
|
+
path,
|
|
216
|
+
message: `Token '${def.id}' is already declared by the base registry — a staging extension may extend the vocabulary, never redefine it.`,
|
|
217
|
+
});
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (!TOKEN_ID_RE.test(def.id)) {
|
|
221
|
+
issues.push({
|
|
222
|
+
code: 'token-namespace-violation',
|
|
223
|
+
tokenId: def.id,
|
|
224
|
+
path,
|
|
225
|
+
message: `Token id '${def.id}' is not '<namespace>:<name>' in kebab-case.`,
|
|
226
|
+
});
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (opts.origin === 'staging' && !EXTENSION_ID_RE.test(def.id)) {
|
|
230
|
+
issues.push({
|
|
231
|
+
code: 'token-namespace-violation',
|
|
232
|
+
tokenId: def.id,
|
|
233
|
+
path,
|
|
234
|
+
message: `Staging token '${def.id}' must be namespaced 'x-<pluginId>:<name>'.`,
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (opts.origin === 'base' && isExtensionTokenId(def.id)) {
|
|
239
|
+
issues.push({
|
|
240
|
+
code: 'token-namespace-violation',
|
|
241
|
+
tokenId: def.id,
|
|
242
|
+
path,
|
|
243
|
+
message: `Base token '${def.id}' may not use the reserved staging 'x-' namespace.`,
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const sampleCheck = def.schema.safeParse(def.sample);
|
|
248
|
+
if (!sampleCheck.success) {
|
|
249
|
+
issues.push({
|
|
250
|
+
code: 'token-sample-invalid',
|
|
251
|
+
tokenId: def.id,
|
|
252
|
+
path,
|
|
253
|
+
message: `Token '${def.id}' sample payload does not satisfy its own schema: ${sampleCheck.error.issues[0]?.message ?? 'invalid'}.`,
|
|
254
|
+
});
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
this.tokens.set(def.id, def);
|
|
258
|
+
this.shapes.set(def.id, describeSchema(def.schema));
|
|
259
|
+
accepted.push(def);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Lattice edges are validated after the batch lands: a staging catalog may
|
|
263
|
+
// declare a child before its parent within the same file.
|
|
264
|
+
for (const def of accepted) {
|
|
265
|
+
const parentId = def.subtypeOf;
|
|
266
|
+
if (parentId === undefined) continue;
|
|
267
|
+
const parent = this.tokens.get(parentId);
|
|
268
|
+
if (!parent) {
|
|
269
|
+
issues.push({
|
|
270
|
+
code: 'token-supertype-missing',
|
|
271
|
+
tokenId: def.id,
|
|
272
|
+
path: opts.path,
|
|
273
|
+
message: `Token '${def.id}' declares supertype '${parentId}', which is not registered.`,
|
|
274
|
+
});
|
|
275
|
+
this.tokens.delete(def.id);
|
|
276
|
+
this.shapes.delete(def.id);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (!isExtensionTokenId(def.id) && isExtensionTokenId(parentId)) {
|
|
280
|
+
issues.push({
|
|
281
|
+
code: 'subtype-inversion',
|
|
282
|
+
tokenId: def.id,
|
|
283
|
+
path: opts.path,
|
|
284
|
+
message: `Core token '${def.id}' declares staging extension '${parentId}' as its supertype — the core would stop compiling when the plugin is removed.`,
|
|
285
|
+
});
|
|
286
|
+
this.tokens.delete(def.id);
|
|
287
|
+
this.shapes.delete(def.id);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (this.hasSubtypeCycle(def.id)) {
|
|
291
|
+
issues.push({
|
|
292
|
+
code: 'token-subtype-cycle',
|
|
293
|
+
tokenId: def.id,
|
|
294
|
+
path: opts.path,
|
|
295
|
+
message: `Token '${def.id}' participates in a subtypeOf cycle.`,
|
|
296
|
+
});
|
|
297
|
+
this.tokens.delete(def.id);
|
|
298
|
+
this.shapes.delete(def.id);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return issues;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
registerMarshaller(marshaller: CCWireMarshaller): void {
|
|
306
|
+
this.marshallers.set(marshaller.tokenId, marshaller);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
get(id: string): TokenDef | undefined {
|
|
310
|
+
return this.tokens.get(id);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
has(id: string): boolean {
|
|
314
|
+
return this.tokens.has(id);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
ids(): string[] {
|
|
318
|
+
return [...this.tokens.keys()].sort();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
marshallerFor(tokenId: string): CCWireMarshaller | undefined {
|
|
322
|
+
return this.marshallers.get(tokenId);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
shapeOf(tokenId: string): SchemaShape | undefined {
|
|
326
|
+
return this.shapes.get(tokenId);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The reflexive-transitive closure of `subtypeOf`: is `childId ⊑ parentId`? */
|
|
330
|
+
isSubtype(childId: string, parentId: string): boolean {
|
|
331
|
+
let cursor: string | undefined = childId;
|
|
332
|
+
const seen = new Set<string>();
|
|
333
|
+
while (cursor !== undefined) {
|
|
334
|
+
if (cursor === parentId) return true;
|
|
335
|
+
if (seen.has(cursor)) return false;
|
|
336
|
+
seen.add(cursor);
|
|
337
|
+
cursor = this.tokens.get(cursor)?.subtypeOf;
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Every ancestor of a token, nearest first — used by V3 diagnostics. */
|
|
343
|
+
ancestorsOf(tokenId: string): string[] {
|
|
344
|
+
const chain: string[] = [];
|
|
345
|
+
const seen = new Set<string>();
|
|
346
|
+
let cursor = this.tokens.get(tokenId)?.subtypeOf;
|
|
347
|
+
while (cursor !== undefined && !seen.has(cursor)) {
|
|
348
|
+
chain.push(cursor);
|
|
349
|
+
seen.add(cursor);
|
|
350
|
+
cursor = this.tokens.get(cursor)?.subtypeOf;
|
|
351
|
+
}
|
|
352
|
+
return chain;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
canConnect(producedId: string, consumedId: string): ConnectResult {
|
|
356
|
+
const produced = this.tokens.get(producedId);
|
|
357
|
+
const consumed = this.tokens.get(consumedId);
|
|
358
|
+
if (!produced) return { ok: false, reason: 'unknown-produced', message: `Token '${producedId}' is not registered.` };
|
|
359
|
+
if (!consumed) return { ok: false, reason: 'unknown-consumed', message: `Token '${consumedId}' is not registered.` };
|
|
360
|
+
if (produced.mode !== consumed.mode)
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
reason: 'mode-mismatch',
|
|
364
|
+
message: `'${producedId}' is a ${produced.mode} port and '${consumedId}' is a ${consumed.mode} port.`,
|
|
365
|
+
};
|
|
366
|
+
if (!this.isSubtype(producedId, consumedId))
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
reason: 'not-a-subtype',
|
|
370
|
+
message: `'${producedId}' is not '${consumedId}' or a subtype of it (lattice: ${
|
|
371
|
+
[producedId, ...this.ancestorsOf(producedId)].join(' ⊑ ')
|
|
372
|
+
}).`,
|
|
373
|
+
};
|
|
374
|
+
return { ok: true };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private hasSubtypeCycle(startId: string): boolean {
|
|
378
|
+
const seen = new Set<string>();
|
|
379
|
+
let cursor: string | undefined = startId;
|
|
380
|
+
while (cursor !== undefined) {
|
|
381
|
+
if (seen.has(cursor)) return true;
|
|
382
|
+
seen.add(cursor);
|
|
383
|
+
cursor = this.tokens.get(cursor)?.subtypeOf;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* ---------------------------------------------------------------------------
|
|
390
|
+
* The base registry. Every id here is part of the app's public wire vocabulary;
|
|
391
|
+
* removing one is a breaking change that goes through the deprecation flow
|
|
392
|
+
* (02 §3.3 step 3), never a delete.
|
|
393
|
+
* ------------------------------------------------------------------------ */
|
|
394
|
+
|
|
395
|
+
const MediaItemSchema = z.object({
|
|
396
|
+
id: z.string(),
|
|
397
|
+
kind: z.enum(['video', 'photo', 'file', 'audio']),
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const VideoItemSchema = z.object({
|
|
401
|
+
id: z.string(),
|
|
402
|
+
kind: z.enum(['video', 'photo', 'file', 'audio']),
|
|
403
|
+
streamUrl: z.string().optional(),
|
|
404
|
+
hlsUrl: z.string().optional(),
|
|
405
|
+
hasHls: z.boolean().optional(),
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
export const BASE_TOKENS: readonly TokenDef[] = [
|
|
409
|
+
{
|
|
410
|
+
id: 'media:items',
|
|
411
|
+
mode: 'value',
|
|
412
|
+
description: 'Any addressable media collection — the lattice root every media feed narrows.',
|
|
413
|
+
schema: z.object({ items: z.array(MediaItemSchema) }),
|
|
414
|
+
sample: { items: [{ id: 'itm_1', kind: 'file' as const }] },
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: 'media:videos',
|
|
418
|
+
mode: 'value',
|
|
419
|
+
subtypeOf: 'media:items',
|
|
420
|
+
description: 'Video collection with per-item playback URLs; degrades to progressive when hls is off.',
|
|
421
|
+
schema: z.object({ items: z.array(VideoItemSchema) }),
|
|
422
|
+
sample: { items: [{ id: 'vid_1', kind: 'video' as const, streamUrl: '/api/asset/stream/vid_1' }] },
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
id: 'media:photos',
|
|
426
|
+
mode: 'value',
|
|
427
|
+
subtypeOf: 'media:items',
|
|
428
|
+
description: 'Photo collection.',
|
|
429
|
+
schema: z.object({ items: z.array(MediaItemSchema) }),
|
|
430
|
+
sample: { items: [{ id: 'pho_1', kind: 'photo' as const }] },
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
id: 'media:video-ref',
|
|
434
|
+
mode: 'value',
|
|
435
|
+
description: 'A single video selection handed between a gallery and an editor.',
|
|
436
|
+
schema: z.object({ id: z.string(), name: z.string().optional() }),
|
|
437
|
+
sample: { id: 'vid_1' },
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
id: 'asset:file-ref',
|
|
441
|
+
mode: 'value',
|
|
442
|
+
description: 'A single stored file reference.',
|
|
443
|
+
schema: z.object({ id: z.string(), mime: z.string().optional() }),
|
|
444
|
+
sample: { id: 'file_1' },
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
id: 'text:content',
|
|
448
|
+
mode: 'value',
|
|
449
|
+
description: 'Plain text body — diff panes, note editors, transcript views.',
|
|
450
|
+
schema: z.object({ text: z.string() }),
|
|
451
|
+
sample: { text: 'hello' },
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
id: 'signal:refresh',
|
|
455
|
+
mode: 'signal',
|
|
456
|
+
description: 'Scoped invalidation pulse. Fan-in target: every emission must carry a scope (03 §6.1).',
|
|
457
|
+
schema: z.object({ scope: z.string().optional() }),
|
|
458
|
+
sample: { scope: 'videos' },
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
id: 'session:private-mode',
|
|
462
|
+
mode: 'value',
|
|
463
|
+
description: 'GuardEngine session assertion. Advisory on the client; every route re-verifies (02 §4.2).',
|
|
464
|
+
schema: z.object({
|
|
465
|
+
unlocked: z.boolean(),
|
|
466
|
+
token: z.string().nullable(),
|
|
467
|
+
claims: z.unknown().nullable(),
|
|
468
|
+
}),
|
|
469
|
+
sample: { unlocked: false, token: null, claims: null },
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: 'nav:path',
|
|
473
|
+
mode: 'value',
|
|
474
|
+
description: 'Current client route path — replaces the deleted Breadcrumbs plugin.',
|
|
475
|
+
schema: z.object({ path: z.string() }),
|
|
476
|
+
sample: { path: '/' },
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
id: 'knowledge:conversation-ref',
|
|
480
|
+
mode: 'value',
|
|
481
|
+
description: 'A RAG conversation reference; deep-link entity for /chat/:conversationId.',
|
|
482
|
+
schema: z.object({ id: z.string() }),
|
|
483
|
+
sample: { id: 'conv_1' },
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
id: 'knowledge:note-ref',
|
|
487
|
+
mode: 'value',
|
|
488
|
+
description: 'A note reference; deep-link entity for note routes.',
|
|
489
|
+
schema: z.object({ id: z.string() }),
|
|
490
|
+
sample: { id: 'note_1' },
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
id: 'knowledge:records',
|
|
494
|
+
mode: 'value',
|
|
495
|
+
external: true,
|
|
496
|
+
description: 'Imported record rows (CSV/sheet/webhook) — external traffic, marshaller-gated (02 §7.1).',
|
|
497
|
+
schema: z.object({ rows: z.array(z.record(z.string(), z.unknown())) }),
|
|
498
|
+
sample: { rows: [{ id: '1' }] },
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
id: 'calendar:reminder',
|
|
502
|
+
mode: 'signal',
|
|
503
|
+
description: 'A due reminder pulse consumed by the notifications inbox.',
|
|
504
|
+
schema: z.object({ scope: z.string().optional(), at: z.number() }),
|
|
505
|
+
sample: { at: 0 },
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
id: 'stopwatch:toggled',
|
|
509
|
+
mode: 'signal',
|
|
510
|
+
description: 'Stopwatch widget start/stop pulse.',
|
|
511
|
+
schema: z.object({ running: z.boolean() }),
|
|
512
|
+
sample: { running: false },
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
id: 'dev:job-status',
|
|
516
|
+
mode: 'value',
|
|
517
|
+
description: 'JobBroker progress for a deploy/pipeline/promotion job.',
|
|
518
|
+
schema: z.object({
|
|
519
|
+
jobId: z.string(),
|
|
520
|
+
state: z.enum(['queued', 'running', 'done', 'failed']),
|
|
521
|
+
rev: z.number().optional(),
|
|
522
|
+
}),
|
|
523
|
+
sample: { jobId: 'job_1', state: 'queued' as const },
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
id: 'nes:rom-url',
|
|
527
|
+
mode: 'value',
|
|
528
|
+
deprecated: { replacedBy: 'asset:file-ref', since: 'core-refactor' },
|
|
529
|
+
description: 'DEPRECATED — the NES toy was pruned from the core catalog (02 §1.6).',
|
|
530
|
+
schema: z.object({ url: z.string() }),
|
|
531
|
+
sample: { url: 'about:blank' },
|
|
532
|
+
},
|
|
533
|
+
];
|
|
534
|
+
|
|
535
|
+
/** Fresh registry per compilation — the compiler never shares mutable state between runs. */
|
|
536
|
+
export const createTokenRegistry = (): TokenRegistry => new TokenRegistry(BASE_TOKENS);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI-scale core (framework-free) — the pure model + the document/localStorage store
|
|
3
|
+
* behind the app-wide "text/UI size" accessibility control. `initUiScale()` applies
|
|
4
|
+
* the saved size on app boot (before React mounts → no flash); the React control +
|
|
5
|
+
* hook live in `cursedbelt/react`.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
clampUiScale,
|
|
9
|
+
DEFAULT_UI_SCALE,
|
|
10
|
+
MAX_UI_SCALE,
|
|
11
|
+
MIN_UI_SCALE,
|
|
12
|
+
nearestUiScaleLevel,
|
|
13
|
+
stepUiScale,
|
|
14
|
+
UI_SCALE_LEVELS,
|
|
15
|
+
UI_SCALE_STEP,
|
|
16
|
+
type UiScaleLevel,
|
|
17
|
+
type UiScaleLevelId,
|
|
18
|
+
uiScaleToFontSize,
|
|
19
|
+
} from './scale';
|
|
20
|
+
export {
|
|
21
|
+
applyUiScaleToDocument,
|
|
22
|
+
createUiScaleStore,
|
|
23
|
+
getUiScale,
|
|
24
|
+
initUiScale,
|
|
25
|
+
setUiScale,
|
|
26
|
+
subscribeUiScale,
|
|
27
|
+
UI_SCALE_STORAGE_KEY,
|
|
28
|
+
type UiScaleStorage,
|
|
29
|
+
type UiScaleStore,
|
|
30
|
+
type UiScaleStoreOptions,
|
|
31
|
+
uiScaleStore,
|
|
32
|
+
} from './uiScaleStore';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI scale — the pure model behind an app-wide "text/UI size" accessibility
|
|
3
|
+
* control. A single number multiplies the document's root font-size, so every
|
|
4
|
+
* **rem-based** Tailwind utility (text, spacing, gaps, rem-sized icons) grows or
|
|
5
|
+
* shrinks together. Because the whole UI scales proportionally — like browser
|
|
6
|
+
* zoom — relative spacing, alignment, and source order are preserved by
|
|
7
|
+
* construction: there's no per-element override that could reflow things into an
|
|
8
|
+
* overlap or reorder a label below its section. (Anything sized in raw px won't
|
|
9
|
+
* scale; cwip's components are rem-first so they do.)
|
|
10
|
+
*
|
|
11
|
+
* This file is framework- and DOM-free so it's trivially testable; the React hook
|
|
12
|
+
* + the store that actually touches `document`/`localStorage` build on it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type UiScaleLevelId = 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
|
|
16
|
+
|
|
17
|
+
export interface UiScaleLevel {
|
|
18
|
+
readonly id: UiScaleLevelId;
|
|
19
|
+
readonly label: string;
|
|
20
|
+
/** Root-font-size multiplier (1 = the browser/app default). */
|
|
21
|
+
readonly scale: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Named presets for a simple picker. Continuous values in between are valid too. */
|
|
25
|
+
export const UI_SCALE_LEVELS: readonly UiScaleLevel[] = [
|
|
26
|
+
{ id: 'sm', label: 'Small', scale: 0.9 },
|
|
27
|
+
{ id: 'md', label: 'Default', scale: 1 },
|
|
28
|
+
{ id: 'lg', label: 'Large', scale: 1.15 },
|
|
29
|
+
{ id: 'xl', label: 'Larger', scale: 1.3 },
|
|
30
|
+
{ id: 'xxl', label: 'Largest', scale: 1.5 },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const MIN_UI_SCALE = 0.8;
|
|
34
|
+
export const MAX_UI_SCALE = 2;
|
|
35
|
+
export const DEFAULT_UI_SCALE = 1;
|
|
36
|
+
/** One press of the +/- stepper. */
|
|
37
|
+
export const UI_SCALE_STEP = 0.1;
|
|
38
|
+
|
|
39
|
+
/** Clamp to the supported range, rounded to 3 decimals; non-finite → default. */
|
|
40
|
+
export function clampUiScale(n: number): number {
|
|
41
|
+
if (!Number.isFinite(n)) return DEFAULT_UI_SCALE;
|
|
42
|
+
return Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Math.round(n * 1000) / 1000));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Nudge a scale up (+1) or down (-1) by one step, clamped. */
|
|
46
|
+
export function stepUiScale(scale: number, dir: 1 | -1): number {
|
|
47
|
+
return clampUiScale(scale + dir * UI_SCALE_STEP);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The preset closest to a (continuous) scale — drives a picker's active state. */
|
|
51
|
+
export function nearestUiScaleLevel(scale: number): UiScaleLevel {
|
|
52
|
+
const s = clampUiScale(scale);
|
|
53
|
+
return UI_SCALE_LEVELS.reduce((best, level) =>
|
|
54
|
+
Math.abs(level.scale - s) < Math.abs(best.scale - s) ? level : best,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The CSS `font-size` value to set on the root, e.g. 1.15 → "115%". Percent (not
|
|
59
|
+
* px) so it composes with the user's own browser base-font preference. */
|
|
60
|
+
export function uiScaleToFontSize(scale: number): string {
|
|
61
|
+
const pct = Math.round(clampUiScale(scale) * 1000) / 10; // 1.15 → 115, 0.9 → 90
|
|
62
|
+
return `${pct}%`;
|
|
63
|
+
}
|