@laplace.live/persona-sdk 1.4.0 → 1.5.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/dist/client/client.d.ts +1 -0
- package/dist/client/client.js +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/values/controller.d.ts +11 -11
- package/dist/values/controller.js +24 -44
- package/dist/values/curve.d.ts +4 -4
- package/dist/values/effect-schema.js +4 -4
- package/dist/values/guards.d.ts +12 -0
- package/dist/values/guards.js +16 -0
- package/dist/values/hands.d.ts +22 -22
- package/dist/values/hands.js +30 -58
- package/dist/values/hotkeys.d.ts +14 -0
- package/dist/values/hotkeys.js +13 -1
- package/dist/values/labels.d.ts +10 -5
- package/dist/values/labels.js +20 -9
- package/dist/values/lipsync.d.ts +2 -2
- package/dist/values/lipsync.js +2 -15
- package/dist/values/locale.d.ts +9 -4
- package/dist/values/locale.js +4 -3
- package/dist/values/scene-transition.d.ts +14 -0
- package/dist/values/scene-transition.js +36 -0
- package/dist/wire/errors.js +3 -2
- package/dist/wire/events.d.ts +4 -4
- package/dist/wire/events.js +3 -2
- package/dist/wire/methods.d.ts +13 -13
- package/dist/wire/protocol.d.ts +3 -1
- package/dist/wire/protocol.js +3 -1
- package/dist/wire/schemas.d.ts +18 -2
- package/dist/wire/schemas.js +17 -7
- package/dist/wire/types.d.ts +78 -21
- package/dist/wire/types.js +60 -66
- package/package.json +1 -1
package/dist/client/client.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export interface PersonaClientOptions {
|
|
|
47
47
|
reconnect?: boolean;
|
|
48
48
|
reconnectDelayMs?: number;
|
|
49
49
|
reconnectDelayMaxMs?: number;
|
|
50
|
+
/** Override every request timeout; defaults to 10 seconds, or 120 seconds for scene activation. */
|
|
50
51
|
requestTimeoutMs?: number;
|
|
51
52
|
/** Bound each awaited {@link PersonaClient.connect} — on expiry it rejects while the attempt keeps going. */
|
|
52
53
|
connectTimeoutMs?: number;
|
package/dist/client/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseServerMessage } from "../wire/envelope.js";
|
|
2
2
|
import { PersonaApiError } from "../wire/errors.js";
|
|
3
|
-
import { CLOSE_FORCE_DISCONNECTED, CLOSE_KEY_REVOKED, INJECT_HEARTBEAT_MS, injectTargetKey, PROTOCOL_VERSION, } from "../wire/protocol.js";
|
|
3
|
+
import { CLOSE_FORCE_DISCONNECTED, CLOSE_KEY_REVOKED, INJECT_HEARTBEAT_MS, injectTargetKey, PROTOCOL_VERSION, SCENE_ACTIVATION_TIMEOUT_MS, } from "../wire/protocol.js";
|
|
4
4
|
import { personaWsUrl } from "./address.js";
|
|
5
5
|
const DEFAULTS = {
|
|
6
6
|
url: personaWsUrl(),
|
|
@@ -107,7 +107,8 @@ export class PersonaClient {
|
|
|
107
107
|
const timer = setTimeout(() => {
|
|
108
108
|
this.pending.delete(id);
|
|
109
109
|
reject(new Error(`request timed out: ${method}`));
|
|
110
|
-
}, this.opts.requestTimeoutMs ??
|
|
110
|
+
}, this.opts.requestTimeoutMs ??
|
|
111
|
+
(method === 'scene.activate' ? SCENE_ACTIVATION_TIMEOUT_MS : DEFAULTS.requestTimeoutMs));
|
|
111
112
|
this.pending.set(id, { resolve: resolve, reject, timer });
|
|
112
113
|
ws.send(JSON.stringify({ kind: 'request', id, method, ...(params === undefined ? {} : { params }) }));
|
|
113
114
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export * from './values/limits.ts';
|
|
|
15
15
|
export * from './values/lipsync.ts';
|
|
16
16
|
export * from './values/locale.ts';
|
|
17
17
|
export * from './values/model-info.ts';
|
|
18
|
+
export * from './values/scene-transition.ts';
|
|
18
19
|
export * from './values/stage-info.ts';
|
|
19
20
|
export * from './values/terms.ts';
|
|
20
21
|
export * from './values/vrm-bindings.ts';
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./values/limits.js";
|
|
|
17
17
|
export * from "./values/lipsync.js";
|
|
18
18
|
export * from "./values/locale.js";
|
|
19
19
|
export * from "./values/model-info.js";
|
|
20
|
+
export * from "./values/scene-transition.js";
|
|
20
21
|
export * from "./values/stage-info.js";
|
|
21
22
|
export * from "./values/terms.js";
|
|
22
23
|
export * from "./values/vrm-bindings.js";
|
|
@@ -2,17 +2,6 @@
|
|
|
2
2
|
export type ControllerSlot = number;
|
|
3
3
|
/** Whether a value can identify a persistent controller profile. */
|
|
4
4
|
export declare function isControllerSlot(value: unknown): value is ControllerSlot;
|
|
5
|
-
/** Standard gamepad controls, using VTS's parameter names for the first controller. */
|
|
6
|
-
export declare const BASE_CONTROLLER_INPUT_NAMES: readonly ["ControllerStickLeftX", "ControllerStickLeftY", "ControllerStickRightX", "ControllerStickRightY", "ControllerStickPressLeft", "ControllerStickPressRight", "ControllerDPadX", "ControllerDPadY", "ControllerCross", "ControllerCircle", "ControllerSquare", "ControllerTriangle", "ControllerShoulderLeft", "ControllerShoulderRight", "ControllerTriggerLeft", "ControllerTriggerRight", "ControllerOptionLeft", "ControllerOptionRight", "ControllerHome"];
|
|
7
|
-
export type BaseControllerInputName = (typeof BASE_CONTROLLER_INPUT_NAMES)[number];
|
|
8
|
-
type NumberedControllerName<T extends string> = T extends `Controller${infer Control}` ? `Controller${number}${Control}` : never;
|
|
9
|
-
export type ControllerInputName = BaseControllerInputName | NumberedControllerName<BaseControllerInputName>;
|
|
10
|
-
/** Address a parameter on one controller; slot 1 retains its original VTS name. */
|
|
11
|
-
export declare function controllerInputName(base: BaseControllerInputName, slot: ControllerSlot): ControllerInputName;
|
|
12
|
-
/** Remove a controller parameter's slot prefix for shared labels and ranges. */
|
|
13
|
-
export declare function baseControllerInputName(full: ControllerInputName): BaseControllerInputName;
|
|
14
|
-
/** Whether an input names a supported controller parameter. */
|
|
15
|
-
export declare function isControllerInputName(v: string): v is ControllerInputName;
|
|
16
5
|
/** Stick and D-pad axes are signed, with positive Y pointing up; other controls are 0..1. */
|
|
17
6
|
export declare const BASE_CONTROLLER_INPUT_RANGES: {
|
|
18
7
|
readonly ControllerStickLeftX: readonly [-1, 1];
|
|
@@ -35,6 +24,17 @@ export declare const BASE_CONTROLLER_INPUT_RANGES: {
|
|
|
35
24
|
readonly ControllerOptionRight: readonly [0, 1];
|
|
36
25
|
readonly ControllerHome: readonly [0, 1];
|
|
37
26
|
};
|
|
27
|
+
/** Standard gamepad controls, using VTS's parameter names for the first controller. */
|
|
28
|
+
export declare const BASE_CONTROLLER_INPUT_NAMES: readonly BaseControllerInputName[];
|
|
29
|
+
export type BaseControllerInputName = keyof typeof BASE_CONTROLLER_INPUT_RANGES;
|
|
30
|
+
type NumberedControllerName<T extends string> = T extends `Controller${infer Control}` ? `Controller${number}${Control}` : never;
|
|
31
|
+
export type ControllerInputName = BaseControllerInputName | NumberedControllerName<BaseControllerInputName>;
|
|
32
|
+
/** Address a parameter on one controller; slot 1 retains its original VTS name. */
|
|
33
|
+
export declare function controllerInputName(base: BaseControllerInputName, slot: ControllerSlot): ControllerInputName;
|
|
34
|
+
/** Remove a controller parameter's slot prefix for shared labels and ranges. */
|
|
35
|
+
export declare function baseControllerInputName(full: ControllerInputName): BaseControllerInputName;
|
|
36
|
+
/** Whether an input names a supported controller parameter. */
|
|
37
|
+
export declare function isControllerInputName(v: string): v is ControllerInputName;
|
|
38
38
|
/** Natural parameter range, shared by every controller profile. */
|
|
39
39
|
export declare function controllerInputRange(name: ControllerInputName): readonly [number, number];
|
|
40
40
|
/** VTS shortcut names differ from parameter names, especially shoulders, clicks and D-pad. */
|
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
import { finiteOr, isRecord, nonEmptyString } from "./guards.js";
|
|
1
|
+
import { finiteOr, isRecord, keysOf, nonEmptyString } from "./guards.js";
|
|
2
2
|
import { clamp } from "./limits.js";
|
|
3
3
|
/** Whether a value can identify a persistent controller profile. */
|
|
4
4
|
export function isControllerSlot(value) {
|
|
5
5
|
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
6
6
|
}
|
|
7
|
+
/** Stick and D-pad axes are signed, with positive Y pointing up; other controls are 0..1. */
|
|
8
|
+
export const BASE_CONTROLLER_INPUT_RANGES = {
|
|
9
|
+
ControllerStickLeftX: [-1, 1],
|
|
10
|
+
ControllerStickLeftY: [-1, 1],
|
|
11
|
+
ControllerStickRightX: [-1, 1],
|
|
12
|
+
ControllerStickRightY: [-1, 1],
|
|
13
|
+
ControllerStickPressLeft: [0, 1],
|
|
14
|
+
ControllerStickPressRight: [0, 1],
|
|
15
|
+
ControllerDPadX: [-1, 1],
|
|
16
|
+
ControllerDPadY: [-1, 1],
|
|
17
|
+
ControllerCross: [0, 1],
|
|
18
|
+
ControllerCircle: [0, 1],
|
|
19
|
+
ControllerSquare: [0, 1],
|
|
20
|
+
ControllerTriangle: [0, 1],
|
|
21
|
+
ControllerShoulderLeft: [0, 1],
|
|
22
|
+
ControllerShoulderRight: [0, 1],
|
|
23
|
+
ControllerTriggerLeft: [0, 1],
|
|
24
|
+
ControllerTriggerRight: [0, 1],
|
|
25
|
+
ControllerOptionLeft: [0, 1],
|
|
26
|
+
ControllerOptionRight: [0, 1],
|
|
27
|
+
ControllerHome: [0, 1],
|
|
28
|
+
};
|
|
7
29
|
/** Standard gamepad controls, using VTS's parameter names for the first controller. */
|
|
8
|
-
export const BASE_CONTROLLER_INPUT_NAMES =
|
|
9
|
-
'ControllerStickLeftX',
|
|
10
|
-
'ControllerStickLeftY',
|
|
11
|
-
'ControllerStickRightX',
|
|
12
|
-
'ControllerStickRightY',
|
|
13
|
-
'ControllerStickPressLeft',
|
|
14
|
-
'ControllerStickPressRight',
|
|
15
|
-
'ControllerDPadX',
|
|
16
|
-
'ControllerDPadY',
|
|
17
|
-
'ControllerCross',
|
|
18
|
-
'ControllerCircle',
|
|
19
|
-
'ControllerSquare',
|
|
20
|
-
'ControllerTriangle',
|
|
21
|
-
'ControllerShoulderLeft',
|
|
22
|
-
'ControllerShoulderRight',
|
|
23
|
-
'ControllerTriggerLeft',
|
|
24
|
-
'ControllerTriggerRight',
|
|
25
|
-
'ControllerOptionLeft',
|
|
26
|
-
'ControllerOptionRight',
|
|
27
|
-
'ControllerHome',
|
|
28
|
-
];
|
|
30
|
+
export const BASE_CONTROLLER_INPUT_NAMES = keysOf(BASE_CONTROLLER_INPUT_RANGES);
|
|
29
31
|
/** Slot 1 keeps the bare VTS name; every other slot inserts its number after `Controller`. */
|
|
30
32
|
function slotPrefixed(base, slot) {
|
|
31
33
|
if (!isControllerSlot(slot))
|
|
@@ -56,28 +58,6 @@ export function isControllerInputName(v) {
|
|
|
56
58
|
const parts = controllerParts(v);
|
|
57
59
|
return parts !== null && CONTROLLER_INPUT_SET.has(parts.base);
|
|
58
60
|
}
|
|
59
|
-
/** Stick and D-pad axes are signed, with positive Y pointing up; other controls are 0..1. */
|
|
60
|
-
export const BASE_CONTROLLER_INPUT_RANGES = {
|
|
61
|
-
ControllerStickLeftX: [-1, 1],
|
|
62
|
-
ControllerStickLeftY: [-1, 1],
|
|
63
|
-
ControllerStickRightX: [-1, 1],
|
|
64
|
-
ControllerStickRightY: [-1, 1],
|
|
65
|
-
ControllerStickPressLeft: [0, 1],
|
|
66
|
-
ControllerStickPressRight: [0, 1],
|
|
67
|
-
ControllerDPadX: [-1, 1],
|
|
68
|
-
ControllerDPadY: [-1, 1],
|
|
69
|
-
ControllerCross: [0, 1],
|
|
70
|
-
ControllerCircle: [0, 1],
|
|
71
|
-
ControllerSquare: [0, 1],
|
|
72
|
-
ControllerTriangle: [0, 1],
|
|
73
|
-
ControllerShoulderLeft: [0, 1],
|
|
74
|
-
ControllerShoulderRight: [0, 1],
|
|
75
|
-
ControllerTriggerLeft: [0, 1],
|
|
76
|
-
ControllerTriggerRight: [0, 1],
|
|
77
|
-
ControllerOptionLeft: [0, 1],
|
|
78
|
-
ControllerOptionRight: [0, 1],
|
|
79
|
-
ControllerHome: [0, 1],
|
|
80
|
-
};
|
|
81
61
|
/** Natural parameter range, shared by every controller profile. */
|
|
82
62
|
export function controllerInputRange(name) {
|
|
83
63
|
return BASE_CONTROLLER_INPUT_RANGES[baseControllerInputName(name)];
|
package/dist/values/curve.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `outRange` is what puts it in Cubism units.
|
|
9
9
|
*/
|
|
10
10
|
/** Governs the segment *leaving* a keyframe; the last keyframe's is never read. */
|
|
11
|
-
export type Interpolation =
|
|
11
|
+
export type Interpolation = (typeof INTERPOLATIONS)[number];
|
|
12
12
|
export interface Point {
|
|
13
13
|
x: number;
|
|
14
14
|
y: number;
|
|
@@ -137,7 +137,7 @@ export declare function insertAnchor(keyframes: Keyframe[], at: Point): {
|
|
|
137
137
|
index: number;
|
|
138
138
|
};
|
|
139
139
|
/** Every {@link Interpolation}, in the order an editor lists them. */
|
|
140
|
-
export declare const INTERPOLATIONS: readonly
|
|
140
|
+
export declare const INTERPOLATIONS: readonly ["linear", "step", "invertStep", "bezier"];
|
|
141
141
|
/**
|
|
142
142
|
* A curve from an untrusted document — a config file or an IPC caller — or null when it
|
|
143
143
|
* cannot be made into one. Anchors are clamped into the unit square and sorted by x, which is
|
|
@@ -154,8 +154,8 @@ export declare function sanitizeCurve(v: unknown): Curve | null;
|
|
|
154
154
|
*/
|
|
155
155
|
export declare function identityCurve(): Curve;
|
|
156
156
|
/** A ready-made response shape that replaces the whole curve; `linear` is the identity. */
|
|
157
|
-
export type CurvePresetId =
|
|
158
|
-
export declare const CURVE_PRESET_IDS: readonly
|
|
157
|
+
export type CurvePresetId = (typeof CURVE_PRESET_IDS)[number];
|
|
158
|
+
export declare const CURVE_PRESET_IDS: readonly ["linear", "step", "invertStep", "bezier", "easeIn", "easeOut", "sCurve", "threshold", "steps"];
|
|
159
159
|
/**
|
|
160
160
|
* Fresh whole-curve shapes: endpoint holds, a Bezier ease-in-out, and shapes from VBridger's presets.
|
|
161
161
|
* The remaining shapes are transcribed from its Unity curves
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Scope and parameter metadata are plain data; labels and rendering stay with their consumers.
|
|
2
|
-
import { finiteOr, isRecord } from "./guards.js";
|
|
2
|
+
import { finiteOr, isOneOf, isRecord } from "./guards.js";
|
|
3
3
|
import { clamp, hexColorOr } from "./limits.js";
|
|
4
4
|
/** Supported owners of each toggle effect; scene and layer membership are independent. */
|
|
5
5
|
export const EFFECT_SCOPES = {
|
|
@@ -28,7 +28,7 @@ export const EFFECT_SCOPES = {
|
|
|
28
28
|
};
|
|
29
29
|
export const EFFECT_KEYS = Object.keys(EFFECT_SCOPES);
|
|
30
30
|
function keysForScope(scope) {
|
|
31
|
-
return EFFECT_KEYS.filter((key) => EFFECT_SCOPES[key]
|
|
31
|
+
return EFFECT_KEYS.filter((key) => isOneOf(scope, EFFECT_SCOPES[key]));
|
|
32
32
|
}
|
|
33
33
|
/** Older hosts advertise effect availability through the values present in their payload. */
|
|
34
34
|
export function availableEffectKeys(scope, effects) {
|
|
@@ -450,9 +450,9 @@ export const ALL_EFFECT_STACK_ORDER = [
|
|
|
450
450
|
'vignette',
|
|
451
451
|
];
|
|
452
452
|
/** Scene composition order, retained under its existing SDK name. */
|
|
453
|
-
export const EFFECT_STACK_ORDER = ALL_EFFECT_STACK_ORDER.filter((key) =>
|
|
453
|
+
export const EFFECT_STACK_ORDER = ALL_EFFECT_STACK_ORDER.filter((key) => isOneOf(key, SCENE_EFFECT_KEYS));
|
|
454
454
|
/** Layer composition order before the result joins the scene. */
|
|
455
|
-
export const LAYER_EFFECT_STACK_ORDER = ALL_EFFECT_STACK_ORDER.filter((key) =>
|
|
455
|
+
export const LAYER_EFFECT_STACK_ORDER = ALL_EFFECT_STACK_ORDER.filter((key) => isOneOf(key, LAYER_EFFECT_KEYS));
|
|
456
456
|
/** Heal only the requested effect keys, using the same parameter kinds as defaults and editors. */
|
|
457
457
|
export function healEffectValues(raw, keys) {
|
|
458
458
|
const src = isRecord(raw) ? raw : {};
|
package/dist/values/guards.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/** Plain-object check: object, non-null, and not an array. */
|
|
2
2
|
export declare function isRecord(v: unknown): v is Record<string, unknown>;
|
|
3
|
+
/**
|
|
4
|
+
* Membership test that narrows. `list.includes(v)` cannot take an `unknown`, and widening the
|
|
5
|
+
* list to `readonly string[]` to get there throws the narrowing away — so every caller ended up
|
|
6
|
+
* asserting the result back. Comparing element-wise keeps `T` in hand.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isOneOf<T extends string>(v: unknown, list: readonly T[]): v is T;
|
|
9
|
+
/**
|
|
10
|
+
* `Object.keys` typed to the record's own keys. TS widens to `string[]` because a value can
|
|
11
|
+
* carry keys its type never listed — true of an open object, not of the closed literal tables
|
|
12
|
+
* this is for, so callers own that the record is exactly its type.
|
|
13
|
+
*/
|
|
14
|
+
export declare function keysOf<T extends object>(o: T): (keyof T & string)[];
|
|
3
15
|
/** Finite-number type guard (rejects NaN and ±Infinity). */
|
|
4
16
|
export declare function isFiniteNumber(v: unknown): v is number;
|
|
5
17
|
/** The value when it is a finite number, else `fallback`. */
|
package/dist/values/guards.js
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
export function isRecord(v) {
|
|
4
4
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Membership test that narrows. `list.includes(v)` cannot take an `unknown`, and widening the
|
|
8
|
+
* list to `readonly string[]` to get there throws the narrowing away — so every caller ended up
|
|
9
|
+
* asserting the result back. Comparing element-wise keeps `T` in hand.
|
|
10
|
+
*/
|
|
11
|
+
export function isOneOf(v, list) {
|
|
12
|
+
return list.some(item => item === v);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* `Object.keys` typed to the record's own keys. TS widens to `string[]` because a value can
|
|
16
|
+
* carry keys its type never listed — true of an open object, not of the closed literal tables
|
|
17
|
+
* this is for, so callers own that the record is exactly its type.
|
|
18
|
+
*/
|
|
19
|
+
export function keysOf(o) {
|
|
20
|
+
return Object.keys(o);
|
|
21
|
+
}
|
|
6
22
|
/** Finite-number type guard (rejects NaN and ±Infinity). */
|
|
7
23
|
export function isFiniteNumber(v) {
|
|
8
24
|
return typeof v === 'number' && Number.isFinite(v);
|
package/dist/values/hands.d.ts
CHANGED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
/** VTube Studio hand inputs, extended with nizima LIVE's wrist Y rotation. */
|
|
2
|
-
export declare const HAND_INPUT_NAMES: readonly ["HandLeftFound", "HandLeftPositionX", "HandLeftPositionY", "HandLeftPositionZ", "HandLeftAngleX", "HandLeftAngleY", "HandLeftAngleZ", "HandLeftOpen", "HandLeftFinger_1_Thumb", "HandLeftFinger_2_Index", "HandLeftFinger_3_Middle", "HandLeftFinger_4_Ring", "HandLeftFinger_5_Pinky", "HandRightFound", "HandRightPositionX", "HandRightPositionY", "HandRightPositionZ", "HandRightAngleX", "HandRightAngleY", "HandRightAngleZ", "HandRightOpen", "HandRightFinger_1_Thumb", "HandRightFinger_2_Index", "HandRightFinger_3_Middle", "HandRightFinger_4_Ring", "HandRightFinger_5_Pinky", "BothHandsFound", "HandDistance"];
|
|
3
|
-
export type HandInputName = (typeof HAND_INPUT_NAMES)[number];
|
|
4
|
-
export type HandInputs = Partial<Record<HandInputName, number>>;
|
|
5
|
-
/** Whether an input uses the canonical hand vocabulary. */
|
|
6
|
-
export declare function isHandInputName(input: string): input is HandInputName;
|
|
7
|
-
/** Names nizima uses for VTS's numbered fingers and both-hands detection. */
|
|
8
|
-
export declare const NIZIMA_HAND_ALIASES: {
|
|
9
|
-
readonly HandLeftFingerThumb: "HandLeftFinger_1_Thumb";
|
|
10
|
-
readonly HandLeftFingerIndex: "HandLeftFinger_2_Index";
|
|
11
|
-
readonly HandLeftFingerMiddle: "HandLeftFinger_3_Middle";
|
|
12
|
-
readonly HandLeftFingerRing: "HandLeftFinger_4_Ring";
|
|
13
|
-
readonly HandLeftFingerPinky: "HandLeftFinger_5_Pinky";
|
|
14
|
-
readonly HandRightFingerThumb: "HandRightFinger_1_Thumb";
|
|
15
|
-
readonly HandRightFingerIndex: "HandRightFinger_2_Index";
|
|
16
|
-
readonly HandRightFingerMiddle: "HandRightFinger_3_Middle";
|
|
17
|
-
readonly HandRightFingerRing: "HandRightFinger_4_Ring";
|
|
18
|
-
readonly HandRightFingerPinky: "HandRightFinger_5_Pinky";
|
|
19
|
-
readonly HandBothFound: "BothHandsFound";
|
|
20
|
-
};
|
|
21
|
-
/** Canonicalize a hand name; aliases share canonical units on the plugin API. */
|
|
22
|
-
export declare function canonicalHandInputName(input: string): HandInputName | null;
|
|
23
1
|
/** Angles are degrees; positions and distance use VTS's ten-unit scale. */
|
|
24
2
|
export declare const HAND_INPUT_RANGES: {
|
|
25
3
|
readonly HandLeftFound: readonly [0, 1];
|
|
@@ -51,3 +29,25 @@ export declare const HAND_INPUT_RANGES: {
|
|
|
51
29
|
readonly BothHandsFound: readonly [0, 1];
|
|
52
30
|
readonly HandDistance: readonly [0, 10];
|
|
53
31
|
};
|
|
32
|
+
/** VTube Studio hand inputs, extended with nizima LIVE's wrist Y rotation. */
|
|
33
|
+
export declare const HAND_INPUT_NAMES: readonly HandInputName[];
|
|
34
|
+
export type HandInputName = keyof typeof HAND_INPUT_RANGES;
|
|
35
|
+
export type HandInputs = Partial<Record<HandInputName, number>>;
|
|
36
|
+
/** Whether an input uses the canonical hand vocabulary. */
|
|
37
|
+
export declare function isHandInputName(input: string): input is HandInputName;
|
|
38
|
+
/** Names nizima uses for VTS's numbered fingers and both-hands detection. */
|
|
39
|
+
export declare const NIZIMA_HAND_ALIASES: {
|
|
40
|
+
readonly HandLeftFingerThumb: "HandLeftFinger_1_Thumb";
|
|
41
|
+
readonly HandLeftFingerIndex: "HandLeftFinger_2_Index";
|
|
42
|
+
readonly HandLeftFingerMiddle: "HandLeftFinger_3_Middle";
|
|
43
|
+
readonly HandLeftFingerRing: "HandLeftFinger_4_Ring";
|
|
44
|
+
readonly HandLeftFingerPinky: "HandLeftFinger_5_Pinky";
|
|
45
|
+
readonly HandRightFingerThumb: "HandRightFinger_1_Thumb";
|
|
46
|
+
readonly HandRightFingerIndex: "HandRightFinger_2_Index";
|
|
47
|
+
readonly HandRightFingerMiddle: "HandRightFinger_3_Middle";
|
|
48
|
+
readonly HandRightFingerRing: "HandRightFinger_4_Ring";
|
|
49
|
+
readonly HandRightFingerPinky: "HandRightFinger_5_Pinky";
|
|
50
|
+
readonly HandBothFound: "BothHandsFound";
|
|
51
|
+
};
|
|
52
|
+
/** Canonicalize a hand name; aliases share canonical units on the plugin API. */
|
|
53
|
+
export declare function canonicalHandInputName(input: string): HandInputName | null;
|
package/dist/values/hands.js
CHANGED
|
@@ -1,61 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export const HAND_INPUT_NAMES = [
|
|
3
|
-
'HandLeftFound',
|
|
4
|
-
'HandLeftPositionX',
|
|
5
|
-
'HandLeftPositionY',
|
|
6
|
-
'HandLeftPositionZ',
|
|
7
|
-
'HandLeftAngleX',
|
|
8
|
-
'HandLeftAngleY',
|
|
9
|
-
'HandLeftAngleZ',
|
|
10
|
-
'HandLeftOpen',
|
|
11
|
-
'HandLeftFinger_1_Thumb',
|
|
12
|
-
'HandLeftFinger_2_Index',
|
|
13
|
-
'HandLeftFinger_3_Middle',
|
|
14
|
-
'HandLeftFinger_4_Ring',
|
|
15
|
-
'HandLeftFinger_5_Pinky',
|
|
16
|
-
'HandRightFound',
|
|
17
|
-
'HandRightPositionX',
|
|
18
|
-
'HandRightPositionY',
|
|
19
|
-
'HandRightPositionZ',
|
|
20
|
-
'HandRightAngleX',
|
|
21
|
-
'HandRightAngleY',
|
|
22
|
-
'HandRightAngleZ',
|
|
23
|
-
'HandRightOpen',
|
|
24
|
-
'HandRightFinger_1_Thumb',
|
|
25
|
-
'HandRightFinger_2_Index',
|
|
26
|
-
'HandRightFinger_3_Middle',
|
|
27
|
-
'HandRightFinger_4_Ring',
|
|
28
|
-
'HandRightFinger_5_Pinky',
|
|
29
|
-
'BothHandsFound',
|
|
30
|
-
'HandDistance',
|
|
31
|
-
];
|
|
32
|
-
const HAND_INPUT_SET = new Set(HAND_INPUT_NAMES);
|
|
33
|
-
/** Whether an input uses the canonical hand vocabulary. */
|
|
34
|
-
export function isHandInputName(input) {
|
|
35
|
-
return HAND_INPUT_SET.has(input);
|
|
36
|
-
}
|
|
37
|
-
/** Names nizima uses for VTS's numbered fingers and both-hands detection. */
|
|
38
|
-
export const NIZIMA_HAND_ALIASES = {
|
|
39
|
-
HandLeftFingerThumb: 'HandLeftFinger_1_Thumb',
|
|
40
|
-
HandLeftFingerIndex: 'HandLeftFinger_2_Index',
|
|
41
|
-
HandLeftFingerMiddle: 'HandLeftFinger_3_Middle',
|
|
42
|
-
HandLeftFingerRing: 'HandLeftFinger_4_Ring',
|
|
43
|
-
HandLeftFingerPinky: 'HandLeftFinger_5_Pinky',
|
|
44
|
-
HandRightFingerThumb: 'HandRightFinger_1_Thumb',
|
|
45
|
-
HandRightFingerIndex: 'HandRightFinger_2_Index',
|
|
46
|
-
HandRightFingerMiddle: 'HandRightFinger_3_Middle',
|
|
47
|
-
HandRightFingerRing: 'HandRightFinger_4_Ring',
|
|
48
|
-
HandRightFingerPinky: 'HandRightFinger_5_Pinky',
|
|
49
|
-
HandBothFound: 'BothHandsFound',
|
|
50
|
-
};
|
|
51
|
-
/** Canonicalize a hand name; aliases share canonical units on the plugin API. */
|
|
52
|
-
export function canonicalHandInputName(input) {
|
|
53
|
-
if (isHandInputName(input))
|
|
54
|
-
return input;
|
|
55
|
-
return Object.hasOwn(NIZIMA_HAND_ALIASES, input)
|
|
56
|
-
? NIZIMA_HAND_ALIASES[input]
|
|
57
|
-
: null;
|
|
58
|
-
}
|
|
1
|
+
import { keysOf } from "./guards.js";
|
|
59
2
|
/** Angles are degrees; positions and distance use VTS's ten-unit scale. */
|
|
60
3
|
export const HAND_INPUT_RANGES = {
|
|
61
4
|
HandLeftFound: [0, 1],
|
|
@@ -87,3 +30,32 @@ export const HAND_INPUT_RANGES = {
|
|
|
87
30
|
BothHandsFound: [0, 1],
|
|
88
31
|
HandDistance: [0, 10],
|
|
89
32
|
};
|
|
33
|
+
/** VTube Studio hand inputs, extended with nizima LIVE's wrist Y rotation. */
|
|
34
|
+
export const HAND_INPUT_NAMES = keysOf(HAND_INPUT_RANGES);
|
|
35
|
+
const HAND_INPUT_SET = new Set(HAND_INPUT_NAMES);
|
|
36
|
+
/** Whether an input uses the canonical hand vocabulary. */
|
|
37
|
+
export function isHandInputName(input) {
|
|
38
|
+
return HAND_INPUT_SET.has(input);
|
|
39
|
+
}
|
|
40
|
+
/** Names nizima uses for VTS's numbered fingers and both-hands detection. */
|
|
41
|
+
export const NIZIMA_HAND_ALIASES = {
|
|
42
|
+
HandLeftFingerThumb: 'HandLeftFinger_1_Thumb',
|
|
43
|
+
HandLeftFingerIndex: 'HandLeftFinger_2_Index',
|
|
44
|
+
HandLeftFingerMiddle: 'HandLeftFinger_3_Middle',
|
|
45
|
+
HandLeftFingerRing: 'HandLeftFinger_4_Ring',
|
|
46
|
+
HandLeftFingerPinky: 'HandLeftFinger_5_Pinky',
|
|
47
|
+
HandRightFingerThumb: 'HandRightFinger_1_Thumb',
|
|
48
|
+
HandRightFingerIndex: 'HandRightFinger_2_Index',
|
|
49
|
+
HandRightFingerMiddle: 'HandRightFinger_3_Middle',
|
|
50
|
+
HandRightFingerRing: 'HandRightFinger_4_Ring',
|
|
51
|
+
HandRightFingerPinky: 'HandRightFinger_5_Pinky',
|
|
52
|
+
HandBothFound: 'BothHandsFound',
|
|
53
|
+
};
|
|
54
|
+
/** Canonicalize a hand name; aliases share canonical units on the plugin API. */
|
|
55
|
+
export function canonicalHandInputName(input) {
|
|
56
|
+
if (isHandInputName(input))
|
|
57
|
+
return input;
|
|
58
|
+
return Object.hasOwn(NIZIMA_HAND_ALIASES, input)
|
|
59
|
+
? NIZIMA_HAND_ALIASES[input]
|
|
60
|
+
: null;
|
|
61
|
+
}
|
package/dist/values/hotkeys.d.ts
CHANGED
|
@@ -9,6 +9,20 @@ export declare function hasCommandModifier(accelerator: string | null): boolean;
|
|
|
9
9
|
export declare function canRunInBackground(accelerator: string | null): boolean;
|
|
10
10
|
/** True when a model hotkey is meant to fire while Persona sits in the background — what main registers on, and what the globe shows. */
|
|
11
11
|
export declare function hotkeyRunsInBackground(h: Hotkey): boolean;
|
|
12
|
+
/** Automations are enabled unless explicitly paused. */
|
|
13
|
+
export declare function automationEnabled(s: {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
}): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* True when an automation claims its combo — what main registers with the OS and what the
|
|
18
|
+
* renderer binds controllers to. The two must agree or a combo fires nothing, or nothing owns it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function automationBindable<T extends {
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
accelerator: string | null;
|
|
23
|
+
}>(s: T): s is T & {
|
|
24
|
+
accelerator: string;
|
|
25
|
+
};
|
|
12
26
|
/** Accelerators bound by more than one entry — the warning every shortcut list raises. */
|
|
13
27
|
export declare function duplicateAccelerators(bindings: readonly {
|
|
14
28
|
accelerator: string | null;
|
package/dist/values/hotkeys.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Hotkey rules every consumer must agree on: main decides OS registration by them, and a
|
|
2
2
|
// client reading `Hotkey.global` off the wire needs the same rule to know the flag can apply.
|
|
3
3
|
import { isControllerTriggerName } from "./controller.js";
|
|
4
|
+
import { isOneOf } from "./guards.js";
|
|
4
5
|
// Shift is deliberately absent: Shift+letter is how text is typed, and a global
|
|
5
6
|
// registration consumes the combo system-wide (VTS can allow it because it observes).
|
|
6
7
|
const COMMAND_MODIFIERS = ['Control', 'Alt', 'Super'];
|
|
@@ -8,7 +9,7 @@ const COMMAND_MODIFIERS = ['Control', 'Alt', 'Super'];
|
|
|
8
9
|
export function hasCommandModifier(accelerator) {
|
|
9
10
|
if (accelerator === null)
|
|
10
11
|
return false;
|
|
11
|
-
return accelerator.split('+').some(t =>
|
|
12
|
+
return accelerator.split('+').some(t => isOneOf(t, COMMAND_MODIFIERS));
|
|
12
13
|
}
|
|
13
14
|
/**
|
|
14
15
|
* True when `global` can apply: a command-modifier combo registers with the OS, and a controller
|
|
@@ -24,6 +25,17 @@ export function canRunInBackground(accelerator) {
|
|
|
24
25
|
export function hotkeyRunsInBackground(h) {
|
|
25
26
|
return h.global && h.active && h.action !== null && canRunInBackground(h.accelerator);
|
|
26
27
|
}
|
|
28
|
+
/** Automations are enabled unless explicitly paused. */
|
|
29
|
+
export function automationEnabled(s) {
|
|
30
|
+
return s.enabled !== false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* True when an automation claims its combo — what main registers with the OS and what the
|
|
34
|
+
* renderer binds controllers to. The two must agree or a combo fires nothing, or nothing owns it.
|
|
35
|
+
*/
|
|
36
|
+
export function automationBindable(s) {
|
|
37
|
+
return automationEnabled(s) && s.accelerator !== null;
|
|
38
|
+
}
|
|
27
39
|
/** Accelerators bound by more than one entry — the warning every shortcut list raises. */
|
|
28
40
|
export function duplicateAccelerators(bindings) {
|
|
29
41
|
const seen = new Set();
|
package/dist/values/labels.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AutomationInfo } from '../wire/types.ts';
|
|
2
2
|
/** Last path segment, tolerating both `/` and `\` separators. */
|
|
3
3
|
export declare function fileBasename(path: string): string;
|
|
4
4
|
/**
|
|
@@ -7,7 +7,12 @@ export declare function fileBasename(path: string): string;
|
|
|
7
7
|
* so the basename is the only identifier there is.
|
|
8
8
|
*/
|
|
9
9
|
export declare function motionLabel(file: string): string;
|
|
10
|
-
/** English label for one
|
|
11
|
-
export declare function
|
|
12
|
-
/**
|
|
13
|
-
|
|
10
|
+
/** English label for one automation action kind; kinds newer than this SDK read `Action`. */
|
|
11
|
+
export declare function automationActionLabel(kind: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Join one automation's action labels: a wait ends a batch, so it reads as an arrow, while
|
|
14
|
+
* actions inside a batch read as ` + `. `kinds` must match `labels` in length and position.
|
|
15
|
+
*/
|
|
16
|
+
export declare function joinAutomationActionLabels(labels: readonly string[], kinds: readonly string[]): string;
|
|
17
|
+
/** Display label for an app automation: its title, else its joined action-kind labels. */
|
|
18
|
+
export declare function automationLabel(automation: Pick<AutomationInfo, 'title' | 'actionKinds'>): string;
|
package/dist/values/labels.js
CHANGED
|
@@ -12,21 +12,32 @@ export function motionLabel(file) {
|
|
|
12
12
|
return base.replace(/\.motion3\.json$|\.exp3\.json$|\.vrma$|\.json$/i, '') || base;
|
|
13
13
|
}
|
|
14
14
|
// English fallbacks; localizing clients keep their own map and use these for unknown kinds.
|
|
15
|
-
const
|
|
15
|
+
const AUTOMATION_ACTION_KIND_LABELS = {
|
|
16
16
|
'effect-toggle': 'Toggle Effect',
|
|
17
17
|
'effect-params': 'Effect Settings',
|
|
18
18
|
'camera-pose': 'Camera Position',
|
|
19
19
|
'reset-camera': 'Reset Camera',
|
|
20
20
|
'layer-visibility': 'Layer Visibility',
|
|
21
21
|
'stream-mode': 'Stream Mode',
|
|
22
|
+
delay: 'Wait',
|
|
22
23
|
};
|
|
23
|
-
/** English label for one
|
|
24
|
-
export function
|
|
25
|
-
return
|
|
24
|
+
/** English label for one automation action kind; kinds newer than this SDK read `Action`. */
|
|
25
|
+
export function automationActionLabel(kind) {
|
|
26
|
+
return AUTOMATION_ACTION_KIND_LABELS[kind] ?? 'Action';
|
|
26
27
|
}
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Join one automation's action labels: a wait ends a batch, so it reads as an arrow, while
|
|
30
|
+
* actions inside a batch read as ` + `. `kinds` must match `labels` in length and position.
|
|
31
|
+
*/
|
|
32
|
+
export function joinAutomationActionLabels(labels, kinds) {
|
|
33
|
+
if (labels.length !== kinds.length)
|
|
34
|
+
throw new RangeError('automation action labels and kinds must have the same length');
|
|
35
|
+
return labels.reduce((out, label, i) => out + (i === 0 ? '' : kinds[i] === 'delay' || kinds[i - 1] === 'delay' ? ' → ' : ' + ') + label, '');
|
|
36
|
+
}
|
|
37
|
+
/** Display label for an app automation: its title, else its joined action-kind labels. */
|
|
38
|
+
export function automationLabel(automation) {
|
|
39
|
+
if (automation.title !== null && automation.title !== '')
|
|
40
|
+
return automation.title;
|
|
41
|
+
return (joinAutomationActionLabels(automation.actionKinds.map(automationActionLabel), automation.actionKinds) ||
|
|
42
|
+
'Automation');
|
|
32
43
|
}
|
package/dist/values/lipsync.d.ts
CHANGED
|
@@ -72,8 +72,6 @@ export declare function healLipSyncProfile(raw: unknown): LipSyncProfile | null;
|
|
|
72
72
|
export declare function healLipSyncConfig(raw: unknown): LipSyncConfig;
|
|
73
73
|
/** Whether a persisted per-model mode is supported. */
|
|
74
74
|
export declare function isLipSyncMode(value: unknown): value is LipSyncMode;
|
|
75
|
-
export declare const VOICE_INPUT_NAMES: readonly ["VoiceVolume", "VoiceFrequency", "VoiceVolumePlusMouthOpen", "VoiceFrequencyPlusMouthSmile", "VoiceA", "VoiceI", "VoiceU", "VoiceE", "VoiceO", "VoiceSilence", "VoiceMouthOpen", "VoiceMouthSpread"];
|
|
76
|
-
export type VoiceInputName = (typeof VOICE_INPUT_NAMES)[number];
|
|
77
75
|
export declare const VOICE_INPUT_RANGES: {
|
|
78
76
|
readonly VoiceVolume: readonly [0, 1];
|
|
79
77
|
readonly VoiceFrequency: readonly [0, 1];
|
|
@@ -88,6 +86,8 @@ export declare const VOICE_INPUT_RANGES: {
|
|
|
88
86
|
readonly VoiceMouthOpen: readonly [0, 1];
|
|
89
87
|
readonly VoiceMouthSpread: readonly [-1, 1];
|
|
90
88
|
};
|
|
89
|
+
export declare const VOICE_INPUT_NAMES: readonly VoiceInputName[];
|
|
90
|
+
export type VoiceInputName = keyof typeof VOICE_INPUT_RANGES;
|
|
91
91
|
/** Semantic import aliases; source analyzer amplitudes need not match Persona's. */
|
|
92
92
|
export declare const NIZIMA_VOICE_ALIASES: {
|
|
93
93
|
readonly LipSyncVolume: "VoiceVolume";
|
package/dist/values/lipsync.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { finiteOr, isFiniteNumber, isRecord, nonEmptyString } from "./guards.js";
|
|
1
|
+
import { finiteOr, isFiniteNumber, isRecord, keysOf, nonEmptyString } from "./guards.js";
|
|
2
2
|
import { clamp } from "./limits.js";
|
|
3
3
|
export const LIP_SYNC_VOWELS = ['A', 'I', 'U', 'E', 'O'];
|
|
4
4
|
export const LIP_SYNC_PHONEMES = [...LIP_SYNC_VOWELS, 'S'];
|
|
@@ -56,20 +56,6 @@ export function healLipSyncConfig(raw) {
|
|
|
56
56
|
export function isLipSyncMode(value) {
|
|
57
57
|
return LIP_SYNC_MODES.some(mode => mode === value);
|
|
58
58
|
}
|
|
59
|
-
export const VOICE_INPUT_NAMES = [
|
|
60
|
-
'VoiceVolume',
|
|
61
|
-
'VoiceFrequency',
|
|
62
|
-
'VoiceVolumePlusMouthOpen',
|
|
63
|
-
'VoiceFrequencyPlusMouthSmile',
|
|
64
|
-
'VoiceA',
|
|
65
|
-
'VoiceI',
|
|
66
|
-
'VoiceU',
|
|
67
|
-
'VoiceE',
|
|
68
|
-
'VoiceO',
|
|
69
|
-
'VoiceSilence',
|
|
70
|
-
'VoiceMouthOpen',
|
|
71
|
-
'VoiceMouthSpread',
|
|
72
|
-
];
|
|
73
59
|
export const VOICE_INPUT_RANGES = {
|
|
74
60
|
VoiceVolume: [0, 1],
|
|
75
61
|
VoiceFrequency: [0, 1],
|
|
@@ -84,6 +70,7 @@ export const VOICE_INPUT_RANGES = {
|
|
|
84
70
|
VoiceMouthOpen: [0, 1],
|
|
85
71
|
VoiceMouthSpread: [-1, 1],
|
|
86
72
|
};
|
|
73
|
+
export const VOICE_INPUT_NAMES = keysOf(VOICE_INPUT_RANGES);
|
|
87
74
|
/** Semantic import aliases; source analyzer amplitudes need not match Persona's. */
|
|
88
75
|
export const NIZIMA_VOICE_ALIASES = {
|
|
89
76
|
LipSyncVolume: 'VoiceVolume',
|
package/dist/values/locale.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const LOCALES: readonly ["en", "ja", "zh-CN", "zh-TW"];
|
|
3
|
-
export type Locale = (typeof LOCALES)[number];
|
|
1
|
+
export type Locale = keyof typeof LOCALE_LABELS;
|
|
4
2
|
/** `ui.language` setting: an explicit locale, or follow the OS language. */
|
|
5
3
|
export type LanguageSetting = 'system' | Locale;
|
|
6
4
|
/** `ui.theme` setting: an explicit appearance, or follow the OS. */
|
|
7
5
|
export type ThemeSetting = 'system' | 'light' | 'dark';
|
|
8
6
|
/** Native-language display names for the language picker (deliberately untranslated). */
|
|
9
|
-
export declare const LOCALE_LABELS:
|
|
7
|
+
export declare const LOCALE_LABELS: {
|
|
8
|
+
en: string;
|
|
9
|
+
ja: string;
|
|
10
|
+
'zh-CN': string;
|
|
11
|
+
'zh-TW': string;
|
|
12
|
+
};
|
|
13
|
+
/** Locales shipped in the compiled catalogs (`pnpm i18n`). */
|
|
14
|
+
export declare const LOCALES: readonly Locale[];
|
|
10
15
|
/** Best supported locale for a BCP 47-ish tag: exact match first, then fuzzy per language. */
|
|
11
16
|
export declare function matchLocaleTag(tag: string): Locale;
|
package/dist/values/locale.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export const LOCALES = ['en', 'ja', 'zh-CN', 'zh-TW'];
|
|
1
|
+
import { isOneOf, keysOf } from "./guards.js";
|
|
3
2
|
/** Native-language display names for the language picker (deliberately untranslated). */
|
|
4
3
|
export const LOCALE_LABELS = {
|
|
5
4
|
en: 'English',
|
|
@@ -7,9 +6,11 @@ export const LOCALE_LABELS = {
|
|
|
7
6
|
'zh-CN': '简体中文',
|
|
8
7
|
'zh-TW': '繁體中文',
|
|
9
8
|
};
|
|
9
|
+
/** Locales shipped in the compiled catalogs (`pnpm i18n`). */
|
|
10
|
+
export const LOCALES = keysOf(LOCALE_LABELS);
|
|
10
11
|
/** Best supported locale for a BCP 47-ish tag: exact match first, then fuzzy per language. */
|
|
11
12
|
export function matchLocaleTag(tag) {
|
|
12
|
-
if (
|
|
13
|
+
if (isOneOf(tag, LOCALES))
|
|
13
14
|
return tag;
|
|
14
15
|
const t = tag.toLowerCase();
|
|
15
16
|
// Most systems/browsers report `zh-TW`/`zh-HK` with no `hant` subtag, so match the regions too.
|