narraleaf-studio 0.4.0 → 0.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/_api.d.ts CHANGED
@@ -59,6 +59,7 @@ type FileDetails = {
59
59
  encoding: BufferEncoding | null;
60
60
  };
61
61
  type TextEncodingId = "utf8" | "utf8bom" | "utf16le" | "utf16be" | "gbk" | "gb18030" | "big5" | "shiftjis" | "euckr" | "windows1252" | "iso88591";
62
+ type PersistedTextEol = "lf" | "crlf";
62
63
  type FsTextEncoding = BufferEncoding | TextEncodingId;
63
64
  type PluginPermissionPersistence = "temporary" | "permanent";
64
65
  type PluginFileSystemPermissionMode = "read" | "write" | "readwrite";
@@ -679,10 +680,32 @@ type StoryActionPayload = {
679
680
  operation: "setBgm" | "playSound" | "stopSound" | "pauseSound" | "resumeSound" | "setVolume" | "setRate" | "muteSound" | "seekSound";
680
681
  objectName?: string;
681
682
  assetId?: string;
683
+ /**
684
+ * The project audio track this row plays on (`ProjectAudioTrack.id`) — the bus it lands on,
685
+ * the multiplier applied to {@link volume}, and the fade/loop defaults it inherits.
686
+ *
687
+ * Only the rows that CREATE a sound (`setBgm`, `playSound`) carry one: a later `/vol piano`
688
+ * addresses a handle that already has a track, and letting it name a second would be two
689
+ * answers to one question (the compiler reports that as a conflict rather than picking).
690
+ *
691
+ * Spelled `audioTrackId`, not `trackId`: the latter is Story Motion's key for a timeline row,
692
+ * and sharing it would make reference counting across documents ambiguous.
693
+ *
694
+ * Additive: no document written before this carries it, so no schema bump. Absent — and a
695
+ * dangling id, since a track can be deleted — resolves to the built-in for the operation's
696
+ * natural bus, which is exactly what the compiler hard-coded before tracks existed.
697
+ */
698
+ audioTrackId?: string;
699
+ /**
700
+ * Fade for this row, in milliseconds, applied in whichever direction the operation goes.
701
+ * Absent = the track's own `fadeInMs` / `fadeOutMs`.
702
+ */
682
703
  fadeMs?: number;
704
+ /** 0-1 as authored. Multiplied by the track's gain at compile time. */
683
705
  volume?: number;
684
706
  rate?: number;
685
707
  muted?: boolean;
708
+ /** Absent = the track's own loop default. */
686
709
  loop?: boolean;
687
710
  /** `seekSound` — where to move the play head, in milliseconds (seconds on the line). */
688
711
  timeMs?: number;
@@ -1096,10 +1119,22 @@ type AutoSaveEntry = {
1096
1119
  metadata: unknown;
1097
1120
  };
1098
1121
  type AudioClipRegion = {
1099
- /** Offset from the start of the clip, in milliseconds. Where playback starts, and where a loop returns to. */
1122
+ /** Offset from the start of the clip, in milliseconds. Where playback starts. */
1100
1123
  inMs?: number;
1101
1124
  /** Offset from the start of the clip, in milliseconds. Where playback stops, or where a loop turns around. */
1102
1125
  outMs?: number;
1126
+ /**
1127
+ * Offset from the start of the clip, in milliseconds. Where each repeat returns to.
1128
+ *
1129
+ * Absent means "return to {@link inMs}" - a plain loop, and exactly what every record written
1130
+ * before this field existed means. Set past the in point it describes the standard VN
1131
+ * **intro→loop**: `inMs..loopStartMs` plays once, then `loopStartMs..outMs` repeats forever.
1132
+ *
1133
+ * Constrained to `inMs <= loopStartMs < outMs`. A value outside that window is dropped rather
1134
+ * than clamped: clamping would invent a loop point the author never marked, while dropping
1135
+ * degrades to the plain loop the two other markers already describe.
1136
+ */
1137
+ loopStartMs?: number;
1103
1138
  };
1104
1139
  declare const UI_PAGE_ANIMATION_PRESETS: readonly [
1105
1140
  "none",
@@ -1419,6 +1454,12 @@ type PluginContributes = {
1419
1454
  export type PluginManifestV2 = Omit<PluginIdentity, "id" | "name" | "version"> & Required<Pick<PluginIdentity, "id" | "name" | "version">> & {
1420
1455
  manifestVersion: typeof PluginManifestVersion;
1421
1456
  description?: string;
1457
+ /**
1458
+ * Package-relative path to the thumbnail shown beside the plugin's name in
1459
+ * the Launcher list. Square, at most 512x512, and one of the extensions in
1460
+ * `PLUGIN_ICON_EXTENSIONS`. Plugins without one keep the name monogram.
1461
+ */
1462
+ icon?: string;
1422
1463
  entries: PluginManifestEntries;
1423
1464
  contributes?: PluginContributes;
1424
1465
  permissions?: PluginInstallPermission[];
@@ -1460,6 +1501,12 @@ type PluginStatus = "enabled" | "disabled" | "needsAuthorization" | "error";
1460
1501
  export type PluginListItem = {
1461
1502
  pluginId: string;
1462
1503
  manifest: NormalizedPluginManifestV2;
1504
+ /**
1505
+ * `app://` address of the declared icon, absent when the plugin ships none.
1506
+ * Resolved here rather than in the renderer because only the main process
1507
+ * knows where the package landed on disk.
1508
+ */
1509
+ iconUrl?: string;
1463
1510
  installPath: string;
1464
1511
  enabled: boolean;
1465
1512
  builtIn: boolean;
@@ -1729,10 +1776,18 @@ declare const en: {
1729
1776
  readonly label: "Automatic checkpoint interval";
1730
1777
  readonly description: "How long to wait before recording a checkpoint in a project's version history. Only ever records when something has changed. Set to 0 to turn automatic checkpoints off.";
1731
1778
  };
1779
+ readonly checkpointOnClose: {
1780
+ readonly label: "Record a checkpoint when a workspace closes";
1781
+ readonly description: "Record a checkpoint in the project's version history when you close its workspace window, so an unrecorded session is not lost. Independent of the interval above.";
1782
+ };
1732
1783
  readonly versionControlAuthor: {
1733
1784
  readonly label: "Author name";
1734
1785
  readonly description: "Recorded as the author of commits and checkpoints. Leave empty to record NarraLeaf Studio instead.";
1735
1786
  };
1787
+ readonly versionControlAuthorEmail: {
1788
+ readonly label: "Author email";
1789
+ readonly description: "Recorded next to the author name on commits and checkpoints, as \"Name <email>\". Leave empty to record no address.";
1790
+ };
1736
1791
  readonly confirmBeforeClose: {
1737
1792
  readonly label: "Confirm before closing a workspace";
1738
1793
  readonly description: "Ask for confirmation when you close a workspace window.";
@@ -1785,6 +1840,18 @@ declare const en: {
1785
1840
  readonly recentTitle: "Recent Projects";
1786
1841
  readonly openFolder: "Open Folder";
1787
1842
  readonly importProject: "Import Project";
1843
+ readonly clone: {
1844
+ readonly title: "Get a project from a server";
1845
+ readonly addressLabel: "Project address";
1846
+ readonly addressHint: "Ask whoever set up the project for this address.";
1847
+ readonly folderLabel: "Where to put it";
1848
+ readonly folderPlaceholder: "Choose an empty folder";
1849
+ readonly folderHint: "Must be a new or empty folder.";
1850
+ readonly confirm: "Get project";
1851
+ readonly cancel: "Cancel";
1852
+ readonly working: "Copying the project from the server. This can take a while.";
1853
+ readonly error: "Could not get the project from the server.";
1854
+ };
1788
1855
  readonly openNamed: "Open {name}";
1789
1856
  readonly search: {
1790
1857
  readonly placeholder: "Search projects";
@@ -2169,6 +2236,10 @@ declare const en: {
2169
2236
  readonly theme: "Theme switcher";
2170
2237
  readonly zoom: "Zoom level";
2171
2238
  readonly version: "Version";
2239
+ readonly textFileName: "Text file name";
2240
+ readonly textEncoding: "Text encoding";
2241
+ readonly textLineEnding: "Line ending";
2242
+ readonly textSelection: "Cursor position";
2172
2243
  };
2173
2244
  };
2174
2245
  readonly save: {
@@ -2195,6 +2266,7 @@ declare const en: {
2195
2266
  readonly localization: "localization";
2196
2267
  readonly voice: "voice library";
2197
2268
  readonly variables: "variable registry";
2269
+ readonly audioTracks: "audio tracks";
2198
2270
  readonly characters: "characters";
2199
2271
  readonly project: "project settings";
2200
2272
  readonly assets: "asset library";
@@ -2277,6 +2349,33 @@ declare const en: {
2277
2349
  readonly platform: "Version control is not available on this machine.";
2278
2350
  readonly installation: "Version control is not available in this installation of Studio.";
2279
2351
  };
2352
+ readonly server: {
2353
+ readonly title: "Server";
2354
+ readonly none: "Not connected to a server";
2355
+ readonly connect: "Connect to a server";
2356
+ readonly addressLabel: "Server address";
2357
+ readonly addressPlaceholder: "lore://studio.example.lan:41337";
2358
+ readonly save: "Connect";
2359
+ readonly cancel: "Cancel";
2360
+ readonly disconnect: "Disconnect";
2361
+ readonly check: "Check the server";
2362
+ readonly checking: "Checking the server\u2026";
2363
+ readonly notChecked: "Not checked";
2364
+ readonly upToDate: "Up to date";
2365
+ readonly localAhead: "You have versions the server does not";
2366
+ readonly remoteAhead: "The server has versions you do not";
2367
+ readonly diverged: "You and the server have both moved on";
2368
+ readonly unreachable: "Cannot reach this server";
2369
+ readonly unauthorized: "This server did not accept you";
2370
+ readonly push: "Send to server";
2371
+ readonly pushing: "Sending to the server\u2026";
2372
+ readonly pushedAlready: "The server already has these versions";
2373
+ readonly sync: "Get from server";
2374
+ readonly syncing: "Getting versions from the server\u2026";
2375
+ readonly syncedNothing: "Already up to date";
2376
+ };
2377
+ readonly syncConflictTitle: "Some files could not be merged";
2378
+ readonly syncConflictDetail: string;
2280
2379
  };
2281
2380
  readonly keybindings: {
2282
2381
  readonly searchPlaceholder: "Search shortcuts\u2026";
@@ -2331,6 +2430,12 @@ declare const en: {
2331
2430
  readonly undo: "Undo";
2332
2431
  readonly redo: "Redo";
2333
2432
  readonly delete: "Delete Keyframe";
2433
+ readonly prevFrame: "Step Playhead Back One Frame";
2434
+ readonly nextFrame: "Step Playhead Forward One Frame";
2435
+ readonly prevFrames: "Step Playhead Back Ten Frames";
2436
+ readonly nextFrames: "Step Playhead Forward Ten Frames";
2437
+ readonly playheadStart: "Move Playhead to Start";
2438
+ readonly playheadEnd: "Move Playhead to End";
2334
2439
  };
2335
2440
  };
2336
2441
  };
@@ -2610,6 +2715,7 @@ declare const en: {
2610
2715
  readonly lineText: "Text";
2611
2716
  readonly labelName: "Label";
2612
2717
  readonly scene: "Scene";
2718
+ readonly track: "Audio Track";
2613
2719
  readonly displayName: "Display Name";
2614
2720
  readonly seekTime: "Seconds";
2615
2721
  readonly cameraOperation: "Pan / Zoom / Rotate / Darken / Motion / Reset";
@@ -2691,6 +2797,7 @@ declare const en: {
2691
2797
  readonly param: "A numeric parameter of its model, by id";
2692
2798
  };
2693
2799
  readonly scene: "Scene";
2800
+ readonly audioTrack: "Audio track";
2694
2801
  readonly label: "Label in this scene";
2695
2802
  readonly variable: "Variable";
2696
2803
  readonly content: "New content, typed by the target";
@@ -2766,6 +2873,7 @@ declare const en: {
2766
2873
  readonly sceneMusicLoop: "Loop";
2767
2874
  readonly sceneMusicFade: "Fade in (s)";
2768
2875
  readonly sceneMusicLoopRegion: "Loops {from}s \u2013 {to}s";
2876
+ readonly sceneMusicIntroLoop: "Plays {from}s, loops {loop}s \u2013 {to}s";
2769
2877
  readonly sceneMusicFromIn: "Starts at {from}s";
2770
2878
  readonly sceneMusicWholeClip: "Whole clip";
2771
2879
  readonly backgroundResolveError: "Image asset could not be resolved: {error}";
@@ -3265,6 +3373,7 @@ declare const en: {
3265
3373
  unknownAsset: string;
3266
3374
  unknownCharacter: string;
3267
3375
  unknownScene: string;
3376
+ unknownAudioTrack: string;
3268
3377
  unknownLabel: string;
3269
3378
  unknownVariable: string;
3270
3379
  unknownForm: string;
@@ -3599,6 +3708,8 @@ declare const en: {
3599
3708
  readonly storyActionTitle: "Story Action";
3600
3709
  };
3601
3710
  readonly audio: {
3711
+ readonly track: "Track";
3712
+ readonly trackDefault: "Default ({name})";
3602
3713
  readonly soundName: "Sound name";
3603
3714
  readonly bgmAsset: "BGM asset";
3604
3715
  readonly soundAsset: "Sound asset";
@@ -3929,6 +4040,7 @@ declare const en: {
3929
4040
  readonly zoomFit: "Fit whole clip";
3930
4041
  readonly zoomSelection: "Zoom to selection";
3931
4042
  readonly markIn: "Set in point at playhead";
4043
+ readonly markLoop: "Set loop point at playhead";
3932
4044
  readonly markOut: "Set out point at playhead";
3933
4045
  readonly channels: "{count} ch";
3934
4046
  };
@@ -3942,13 +4054,16 @@ declare const en: {
3942
4054
  readonly nudgeForwardCoarse: "Nudge playhead forward one second";
3943
4055
  readonly loop: "Toggle loop";
3944
4056
  readonly markIn: "Set in point";
4057
+ readonly markLoop: "Set loop point";
3945
4058
  readonly markOut: "Set out point";
3946
4059
  readonly goToIn: "Go to in point";
4060
+ readonly goToLoop: "Go to loop point";
3947
4061
  readonly goToOut: "Go to out point";
3948
4062
  readonly clearIn: "Clear in point";
4063
+ readonly clearLoop: "Clear loop point";
3949
4064
  readonly clearOut: "Clear out point";
3950
- readonly undo: "Undo in/out change";
3951
- readonly redo: "Redo in/out change";
4065
+ readonly undo: "Undo marker change";
4066
+ readonly redo: "Redo marker change";
3952
4067
  readonly selectAll: "Select whole clip";
3953
4068
  readonly clearSelection: "Clear selection";
3954
4069
  readonly zoomIn: "Zoom in";
@@ -3989,8 +4104,14 @@ declare const en: {
3989
4104
  readonly loadFailed: "Failed to read this file.";
3990
4105
  readonly saveFailed: "Failed to save this file.";
3991
4106
  readonly caret: "Ln {line}, Col {column}";
4107
+ readonly selected: "({count} selected)";
4108
+ readonly selectedInRanges: "({count} selected in {ranges} ranges)";
4109
+ readonly selectionLabel: "Cursor position and selection";
3992
4110
  readonly reopenWithEncoding: "Reopen with Encoding";
3993
4111
  readonly saveWithEncoding: "Save with Encoding";
4112
+ readonly selectEncoding: "Select the file's encoding";
4113
+ readonly selectLineEnding: "Select the file's line ending";
4114
+ readonly lineEndingLabel: "Line ending: {ending}";
3994
4115
  readonly encodingLabel: "Encoding: {encoding}";
3995
4116
  readonly actionFailed: "This action failed.";
3996
4117
  };
@@ -4343,6 +4464,8 @@ declare const en: {
4343
4464
  readonly untitledSurface: "Untitled surface";
4344
4465
  readonly untitledStory: "Untitled story";
4345
4466
  readonly untitledScene: "Untitled scene";
4467
+ readonly unnamedCharacter: "Unnamed character";
4468
+ readonly missingCharacter: "Missing character ({id})";
4346
4469
  };
4347
4470
  readonly tsPane: {
4348
4471
  readonly importHint: "Allowed import: \"narraleaf-studio\"";
@@ -4385,6 +4508,7 @@ declare const en: {
4385
4508
  readonly array: "Array";
4386
4509
  readonly asset: "Asset";
4387
4510
  readonly autoForward: "Auto Forward";
4511
+ readonly avatar: "Avatar";
4388
4512
  readonly bgmVolume: "BGM Volume";
4389
4513
  readonly body: "Body";
4390
4514
  readonly boolean: "Boolean";
@@ -4395,6 +4519,7 @@ declare const en: {
4395
4519
  readonly case: "Case";
4396
4520
  readonly case0: "Case 0";
4397
4521
  readonly case1: "Case 1";
4522
+ readonly character: "Character";
4398
4523
  readonly circIn: "Circ In";
4399
4524
  readonly circInOut: "Circ In Out";
4400
4525
  readonly circOut: "Circ Out";
@@ -4431,6 +4556,7 @@ declare const en: {
4431
4556
  readonly field: "Field";
4432
4557
  readonly float: "Float";
4433
4558
  readonly fnName: "Fn Name";
4559
+ readonly found: "Found";
4434
4560
  readonly frame: "Frame";
4435
4561
  readonly from: "From";
4436
4562
  readonly function: "Function";
@@ -4465,6 +4591,8 @@ declare const en: {
4465
4591
  readonly loop: "Loop";
4466
4592
  readonly playing: "Playing";
4467
4593
  readonly fadeS: "Fade (s)";
4594
+ readonly fadeInS: "Fade In (s)";
4595
+ readonly audioTrack: "Track";
4468
4596
  readonly timeS: "Time (s)";
4469
4597
  readonly max: "Max";
4470
4598
  readonly maxOffset: "Max Offset";
@@ -4617,6 +4745,7 @@ declare const en: {
4617
4745
  readonly getAvailableLanguages: "Get Available Languages";
4618
4746
  readonly getBacklog: "Get Backlog";
4619
4747
  readonly getBounds: "Get Bounds";
4748
+ readonly getCharacter: "Get Character";
4620
4749
  readonly getChoiceCount: "Get Choice Count";
4621
4750
  readonly getCurrentLanguage: "Get Current Language";
4622
4751
  readonly getDisplay: "Get Display";
@@ -4666,6 +4795,7 @@ declare const en: {
4666
4795
  readonly getSliderRange: "Get Slider Range";
4667
4796
  readonly getSliderValue: "Get Slider Value";
4668
4797
  readonly getSpeakerAvatar: "Get Speaker Avatar";
4798
+ readonly getSpeakerColor: "Get Speaker Color";
4669
4799
  readonly getText: "Get Text";
4670
4800
  readonly getTextAlign: "Get Text Align";
4671
4801
  readonly getTextColor: "Get Text Color";
@@ -4974,18 +5104,27 @@ declare const en: {
4974
5104
  readonly customNameHint: "Also the folder it lives in, under runtimes/puppet/.";
4975
5105
  };
4976
5106
  readonly poses: "Poses";
4977
- readonly newPose: "New pose";
4978
5107
  readonly removePose: "Remove pose";
5108
+ readonly defaultPoseName: "Pose {n}";
5109
+ readonly defaultAxisName: "Axis {n}";
5110
+ readonly defaultTagName: "Tag {n}";
5111
+ readonly defaultLayerName: "Layer {n}";
5112
+ readonly renameHint: "Double-click or F2 to rename";
4979
5113
  readonly avatar: "Dialog avatar";
4980
5114
  readonly avatarAxis: "Avatar varies with this axis";
5115
+ readonly avatarClearOverride: "Remove override";
5116
+ readonly rebake: "Re-bake avatars";
5117
+ readonly bakeReceipt: "{written} written \u00B7 {unresolved} without art \u00B7 {removed} removed";
5118
+ readonly selectImage: "Choose an image";
5119
+ readonly selectAvatarImage: "Choose an avatar image";
4981
5120
  readonly axes: "Axes";
4982
- readonly newAxis: "New axis";
4983
5121
  readonly removeAxis: "Remove axis";
4984
5122
  readonly newTag: "New tag";
4985
5123
  readonly removeTag: "Remove tag";
4986
5124
  readonly makeDefault: "Make default";
5125
+ readonly isDefault: "Default";
5126
+ readonly changeImage: "Change image";
4987
5127
  readonly layers: "Layers";
4988
- readonly newLayer: "New layer";
4989
5128
  readonly removeLayer: "Remove layer";
4990
5129
  readonly constantLayer: "Always drawn";
4991
5130
  readonly hasImage: "image set";
@@ -5038,54 +5177,28 @@ declare const en: {
5038
5177
  readonly axisNoTags: "{name} has no tags";
5039
5178
  readonly axisUnused: "{name} drives no layer";
5040
5179
  readonly duplicateTag: "{axis} has two tags named {name}";
5180
+ readonly duplicateAxis: "Two axes are named {name}";
5181
+ readonly axisDefaultMissing: "{axis} has no default tag; {name} is used instead";
5041
5182
  readonly occluded: "{name} is completely covered by the layers above it";
5042
5183
  readonly avatarCombinations: "{count} dialog avatars will be baked; narrow the axes to reduce it";
5184
+ readonly combinationNoArt: "{name} draws nothing at all";
5185
+ readonly snapshotStale: "{name} was saved with tags that no longer exist";
5186
+ readonly poseNoImage: "{name} has no image";
5187
+ readonly noPoses: "This character has no poses, so nothing draws";
5188
+ readonly defaultPoseMissing: "The default pose was deleted; {name} is used instead";
5189
+ readonly duplicatePose: "Two poses are named {name}";
5043
5190
  };
5044
5191
  readonly header: {
5045
5192
  readonly fallbackName: "Character";
5046
5193
  readonly subtitle: "Character Editor";
5047
5194
  };
5048
- readonly selectVariantImage: "Select Variant Image";
5049
- readonly dialog: {
5050
- readonly newFormTitle: "New Form";
5051
- readonly formNamePlaceholder: "Enter form name";
5052
- readonly renameFormTitle: "Rename Form";
5053
- readonly newVariantTitle: "New Variant";
5054
- readonly variantNamePlaceholder: "Enter variant name";
5055
- readonly newGroupTitle: "New Variant Group";
5056
- readonly groupNamePlaceholder: "Enter group name";
5057
- readonly renameGroupTitle: "Rename Group";
5058
- readonly renameVariantTitle: "Rename Variant";
5059
- };
5060
- readonly validation: {
5061
- readonly nameRequired: "Name is required";
5062
- readonly formExists: "Form already exists";
5063
- readonly formNotFound: "Form not found";
5064
- readonly variantExists: "Variant already exists in this form";
5065
- readonly groupExists: "Group already exists";
5066
- };
5067
- readonly notify: {
5068
- readonly renameFormFailed: "Unable to rename form";
5069
- readonly variantNameUnique: "Variant name must be unique within the form";
5070
- readonly deleteUngroupedBucket: "Cannot delete ungrouped bucket";
5071
- readonly renameUngroupedBucket: "Cannot rename ungrouped bucket";
5072
- readonly renameGroupFailed: "Unable to rename group";
5073
- readonly renameVariantFailed: "Unable to rename variant";
5074
- };
5075
5195
  readonly confirm: {
5076
- readonly deleteFormTitle: "Delete form \"{name}\"?";
5077
- readonly deleteFormDetail: "Groups and variants inside will be removed.";
5078
- readonly deleteGroupTitle: "Delete group \"{name}\"?";
5079
- readonly deleteGroupDetail: "Variants inside will be removed.";
5080
- readonly deleteVariantTitle: "Delete variant \"{name}\"?";
5081
- readonly deleteVariantDetail: "Asset binding will be removed.";
5082
- };
5083
- readonly menu: {
5084
- readonly renameVariant: "Rename Variant";
5085
- readonly setAsDefault: "Set as Default";
5086
- readonly deleteVariant: "Delete Variant";
5087
- readonly renameForm: "Rename Form";
5088
- readonly deleteForm: "Delete Form";
5196
+ readonly deleteTitle: "Delete \"{name}\"?";
5197
+ readonly deletePoseDetail: "Story rows that name this pose will no longer resolve one.";
5198
+ readonly deleteAxisDetail: "Layers bound to it become constant and lose their per-tag images.";
5199
+ readonly deleteTagDetail: "Images assigned to this tag are released.";
5200
+ readonly deleteLayerDetail: "Its images are released.";
5201
+ readonly deleteSnapshotDetail: "The combination itself is unaffected \u2014 only the name is removed.";
5089
5202
  };
5090
5203
  };
5091
5204
  readonly properties: {
@@ -5093,6 +5206,9 @@ declare const en: {
5093
5206
  readonly thumbnail: "Thumbnail";
5094
5207
  readonly preview: "Preview";
5095
5208
  readonly defaultAvatar: "Dialog avatar";
5209
+ readonly voiceTrack: "Voice bus";
5210
+ readonly voiceTrackMissing: "Missing bus";
5211
+ readonly voiceTrackEmpty: "Add a bus under Voice in Project \u2192 Audio to give this character its own volume.";
5096
5212
  readonly select: "Select";
5097
5213
  readonly thumbnailAlt: "thumbnail";
5098
5214
  readonly color: "Color";
@@ -5116,18 +5232,6 @@ declare const en: {
5116
5232
  readonly unknown: "Unknown error";
5117
5233
  };
5118
5234
  };
5119
- readonly formsPanel: {
5120
- readonly title: "Forms";
5121
- readonly addForm: "Add form";
5122
- readonly expand: "Expand forms";
5123
- readonly collapse: "Collapse forms";
5124
- readonly default: "Default";
5125
- readonly formActions: "Form actions";
5126
- readonly groupCount: {
5127
- readonly one: "{count} group";
5128
- readonly other: "{count} groups";
5129
- };
5130
- };
5131
5235
  readonly preview: {
5132
5236
  readonly title: "Preview";
5133
5237
  readonly currentForm: "Current form: {name}";
@@ -5142,30 +5246,9 @@ declare const en: {
5142
5246
  readonly resetView: "Reset View";
5143
5247
  readonly alt: "Variant preview";
5144
5248
  readonly setPortrait: "Portrait framing";
5145
- readonly resetPortrait: "Reset portrait framing";
5249
+ readonly resetPortrait: "Frame automatically";
5146
5250
  readonly portraitTitle: "Portrait framing";
5147
- readonly portraitFormScopedOff: "Frame this form only";
5148
- readonly portraitFormScopedOn: "Framing this form only \u2014 click to inherit the default";
5149
- };
5150
- readonly variantsPanel: {
5151
- readonly title: "Variants";
5152
- readonly expand: "Expand variants";
5153
- readonly collapse: "Collapse variants";
5154
- readonly addGroup: "Add group";
5155
- readonly addVariant: "Add variant";
5156
- readonly addVariantToGroup: "Add variant to group";
5157
- readonly ungrouped: "Ungrouped";
5158
- readonly default: "Default";
5159
- readonly changeImage: "Change image";
5160
- readonly variantActions: "Variant actions";
5161
- readonly renameGroup: "Rename group";
5162
- readonly deleteGroup: "Delete group";
5163
- readonly groupPrefix: "Group: {name}";
5164
- readonly selectForm: "Select a form from the left.";
5165
- readonly variantCount: {
5166
- readonly one: "{count} variant";
5167
- readonly other: "{count} variants";
5168
- };
5251
+ readonly portraitScoped: "Frame this look only";
5169
5252
  };
5170
5253
  };
5171
5254
  readonly properties: {
@@ -5976,6 +6059,8 @@ declare const en: {
5976
6059
  readonly autoplayHint: "Applies in the game. The editor canvas stays paused; use the docker bar to preview.";
5977
6060
  readonly volume: "Volume";
5978
6061
  readonly volumeHint: "0 to 1. A blueprint volume pin overrides this at runtime.";
6062
+ readonly audioTrack: "Audio Track";
6063
+ readonly audioTrackDefault: "SFX";
5979
6064
  readonly playbackRate: "Speed";
5980
6065
  readonly controls: "Native controls";
5981
6066
  readonly controlsHint: "Shows the browser's own play bar over the clip.";
@@ -6512,6 +6597,10 @@ declare const en: {
6512
6597
  readonly title: "Game";
6513
6598
  readonly description: "How the finished game behaves for players";
6514
6599
  };
6600
+ readonly audio: {
6601
+ readonly title: "Audio";
6602
+ readonly description: "The mixer: which bus feeds which, and how loud each one is";
6603
+ };
6515
6604
  readonly assets: {
6516
6605
  readonly title: "Assets";
6517
6606
  readonly description: "Application icons for each platform";
@@ -6580,6 +6669,37 @@ declare const en: {
6580
6669
  readonly autoSaveSlotsTitle: "Autosaves kept";
6581
6670
  readonly autoSaveSlotsDescription: "Autosaves rotate through this many slots, oldest overwritten first. They stay out of the player's own save slots and are read with the List Auto Saves node.";
6582
6671
  };
6672
+ readonly audio: {
6673
+ readonly add: "Add track";
6674
+ readonly newTrackName: "New Track";
6675
+ readonly nameTitle: "Name";
6676
+ readonly nameDescription: "What this bus is called across Studio. References are stored by id, so renaming one is safe.";
6677
+ readonly parentTitle: "Routes into";
6678
+ readonly parentDescription: "The bus this one feeds. Every bus between here and the master output multiplies its volume in.";
6679
+ readonly parentMaster: "Master output";
6680
+ readonly volumeTitle: "Volume";
6681
+ readonly volumeDescription: "This bus's own level, applied live to everything playing on it or below it. A bus can quieten, never amplify.";
6682
+ readonly volumeUnit: "%";
6683
+ readonly loopTitle: "Loop by default";
6684
+ readonly loopDescription: "Clips played on this track repeat unless the action that plays them says otherwise.";
6685
+ readonly duplicate: "Duplicate";
6686
+ readonly delete: "Delete";
6687
+ readonly deleteConfirm: "Delete \"{name}\"?";
6688
+ readonly deleteDetail: {
6689
+ readonly one: "{count} reference falls back to its default bus.";
6690
+ readonly other: "{count} references fall back to their default bus.";
6691
+ };
6692
+ readonly deleteChildren: {
6693
+ readonly one: "{count} track under it moves to {parent}.";
6694
+ readonly other: "{count} tracks under it move to {parent}.";
6695
+ };
6696
+ readonly slider: {
6697
+ readonly bgm: "BGM Volume";
6698
+ readonly sound: "Sound Volume";
6699
+ readonly voice: "Voice Volume";
6700
+ readonly global: "Global Volume";
6701
+ };
6702
+ };
6583
6703
  readonly settings: {
6584
6704
  readonly allowHttpTitle: "Allow HTTP";
6585
6705
  readonly allowHttpDescription: "When off, the game is confined to the app protocol and all HTTP/HTTPS requests are blocked.";
@@ -6859,6 +6979,9 @@ declare const en: {
6859
6979
  };
6860
6980
  readonly versionControl: {
6861
6981
  readonly description: "Set up version control for your project.";
6982
+ readonly loreHint: "A version history is created inside the project folder, recording it as the first version.";
6983
+ readonly unavailablePlatform: "Version control is not available on this machine, so the project is created without it.";
6984
+ readonly unavailableInstallation: "Version control is not available in this installation of Studio, so the project is created without it.";
6862
6985
  };
6863
6986
  readonly projectLocation: "Project Location";
6864
6987
  readonly projectLocationPlaceholder: "Enter project location\u2026";
@@ -7279,10 +7402,17 @@ declare const en: {
7279
7402
  readonly aliasLocked: "Enter the keystore password";
7280
7403
  readonly aliasEmpty: "No signing key in this keystore";
7281
7404
  readonly keyPasswordSame: "Same as the keystore password";
7405
+ readonly macIdentityLoading: "Reading the keychain\u2026";
7406
+ readonly macIdentityEmpty: "This Mac holds no code-signing certificate. Install your Developer ID certificate in Keychain Access, or import it as a certificate file instead.";
7407
+ readonly macIdentityNotDeveloperId: "not for distribution";
7408
+ readonly notarized: "Notarized with Apple";
7409
+ readonly notNotarized: "Not notarized - Gatekeeper warns players on first launch";
7282
7410
  readonly kind: {
7283
7411
  readonly "windows-pfx": "Certificate file";
7284
7412
  readonly "windows-store": "Windows certificate store";
7285
7413
  readonly "windows-azure": "Azure Trusted Signing";
7414
+ readonly "macos-keychain": "Certificate in the keychain";
7415
+ readonly "macos-apple": "Certificate file";
7286
7416
  readonly "android-keystore": "Release keystore";
7287
7417
  readonly "ios-apple": "Apple identity";
7288
7418
  readonly "linux-gpg": "GPG key";
@@ -7306,6 +7436,10 @@ declare const en: {
7306
7436
  readonly publisher: "Publisher";
7307
7437
  readonly keyId: "Key ID";
7308
7438
  readonly gpgPath: "gpg path";
7439
+ readonly macIdentity: "Certificate";
7440
+ readonly notaryKey: "Notary key (.p8)";
7441
+ readonly notaryKeyId: "Notary key ID";
7442
+ readonly notaryIssuerId: "Notary issuer ID";
7309
7443
  };
7310
7444
  };
7311
7445
  readonly output: {
@@ -7350,8 +7484,11 @@ declare const en: {
7350
7484
  readonly "signing-credential-expiring": "The {platform} signing certificate expires on {notAfter}, in {days} day(s). Builds signed and timestamped before then stay valid afterwards, but new ones need a renewed certificate.";
7351
7485
  readonly "signing-secret-unavailable": "The password for the {platform} signing credential cannot be read on this machine: it is sealed with the system keyring, which is unavailable here or belonged to a different user account. Import the credential again to store the password afresh.";
7352
7486
  readonly "signing-tool-missing": "Signing the {platform} build needs {tool}, which this machine does not have. Install it and make sure it is on your PATH, then reopen this dialog.";
7353
- readonly "signing-host-unsupported": "This machine runs {host}, which cannot sign the {platform} build with the selected credential: a certificate held in the Windows certificate store is only reachable from Windows. Use a certificate file instead, or build this target on a Windows machine.";
7354
- readonly "signing-needs-network": "Signing the {platform} build reaches the network - the signature is timestamped by a certificate authority, cloud signing runs remotely, and a machine without the Windows SDK downloads the signing tool first. Everything else about this build works offline.";
7487
+ readonly "signing-host-unsupported": "This machine runs {host}, which cannot sign the {platform} build with the selected credential: its private key is held by an operating-system service that only exists on that platform - the Windows certificate store, or the macOS keychain that Apple's codesign reads. Build this target on a machine of that platform.";
7488
+ readonly "signing-needs-network": "Signing the {platform} build reaches the network: signatures are timestamped by a certificate authority, cloud signing and Apple notarization run remotely, and a host missing a signing tool downloads it first. Everything else about this build works offline.";
7489
+ readonly "signing-macos-identity-missing": "No certificate named {identity} is in this Mac's keychain, so codesign has nothing to sign with. Install it in Keychain Access, or choose another certificate here.";
7490
+ readonly "signing-macos-identity-unusable": "The certificate {identity} is in this Mac's keychain, but it cannot sign: it has expired, its private key is not there, or its issuing chain is incomplete. Open it in Keychain Access to see which - a certificate imported without Apple's intermediate certificate is the usual cause.";
7491
+ readonly "signing-macos-not-developer-id": "{identity} is not a \"Developer ID Application\" certificate. The build will be signed and will run on this Mac, but Gatekeeper refuses it on anyone else's and Apple will not notarize it. Only a Developer ID certificate works for software distributed outside the App Store.";
7355
7492
  readonly "signing-android-not-play": "The signed APK is for sideloading and for stores that accept APKs, such as itch.io. Google Play is not one of them: it accepts only AAB packages, which this pipeline does not produce.";
7356
7493
  readonly "signing-ios-profile-mismatch": "The app id {bundleId} is not covered by the provisioning profile, which is issued for {profileAppId}. iOS refuses to install an app the profile does not name - change the project identifier, or import the profile that covers it.";
7357
7494
  readonly "cross-build-download": "Cross-building for {platforms} downloads Electron on first use (cached afterwards).";
@@ -7390,10 +7527,6 @@ declare const en: {
7390
7527
  readonly one: "{count} word";
7391
7528
  readonly other: "{count} words";
7392
7529
  };
7393
- readonly nodes: {
7394
- readonly one: "{count} node";
7395
- readonly other: "{count} nodes";
7396
- };
7397
7530
  readonly days: {
7398
7531
  readonly one: "{count} day";
7399
7532
  readonly other: "{count} days";
@@ -7418,7 +7551,7 @@ declare const en: {
7418
7551
  readonly totalWords: "Words";
7419
7552
  readonly characters: "Characters";
7420
7553
  readonly assets: "Assets";
7421
- readonly blueprints: "Blueprints";
7554
+ readonly blueprintNodes: "Blueprint nodes";
7422
7555
  readonly uiSurfaces: "Interface surfaces";
7423
7556
  readonly variables: "Variables";
7424
7557
  };
@@ -7499,7 +7632,7 @@ declare const en: {
7499
7632
  readonly message: "{asset} collides with {other} on case-insensitive filesystems";
7500
7633
  };
7501
7634
  readonly portabilityMediaFormat: {
7502
- readonly title: "Unsupported media format";
7635
+ readonly title: "Unplayable format";
7503
7636
  readonly description: "A codec some selected build targets cannot play";
7504
7637
  readonly message: "{asset} does not play on {platform}";
7505
7638
  };
@@ -7960,6 +8093,29 @@ interface AssetExtras {
7960
8093
  * extra state at all, and re-detection keeps working if a later Studio learns a new format.
7961
8094
  */
7962
8095
  modelEntry?: string;
8096
+ /**
8097
+ * Text assets only: the encoding the author said this file is in.
8098
+ *
8099
+ * A property of the *file*, not of the window, which is why it rides the record into version
8100
+ * control rather than sitting in session state: the point of a shared plan file is that the
8101
+ * colleague who opens the GBK spreadsheet next gets it right without having to know. Outranks
8102
+ * the byte-order-mark sniff on open, because an author who said so has said more than a
8103
+ * heuristic can.
8104
+ *
8105
+ * Written only when the author explicitly reopens or saves under an encoding. Absent - the
8106
+ * normal state - means "sniff the BOM, then UTF-8", so merely reading a file never produces a
8107
+ * change to commit.
8108
+ */
8109
+ textEncoding?: TextEncodingId;
8110
+ /**
8111
+ * Text assets only: the line ending this file uses.
8112
+ *
8113
+ * Recorded for the same reason as {@link textEncoding}, and needed at all because a *new* text
8114
+ * file is zero bytes: there is nothing in the content to detect, so the platform that created it
8115
+ * is the only thing that can answer. For a file that has content, the content wins - see
8116
+ * `resolveLineEnding` - and this is only the fallback.
8117
+ */
8118
+ textEol?: PersistedTextEol;
7963
8119
  /**
7964
8120
  * Superseded by {@link audioLoop}, which replaced a free list of markers with the one in/out
7965
8121
  * pair a clip actually has. Read only so records written by the short-lived cue-point model
@@ -8027,8 +8183,14 @@ type UIListItemScope = {
8027
8183
  count: number;
8028
8184
  key: string;
8029
8185
  };
8030
- declare const BLUEPRINT_HOST_API_CONTRACT_VERSION: 29;
8186
+ declare const BLUEPRINT_HOST_API_CONTRACT_VERSION: 30;
8031
8187
  type BlueprintHostApiContractVersion = typeof BLUEPRINT_HOST_API_CONTRACT_VERSION;
8188
+ type BlueprintRGBAColor = {
8189
+ r: number;
8190
+ g: number;
8191
+ b: number;
8192
+ a: number;
8193
+ };
8032
8194
  type BlueprintImageAsset = {
8033
8195
  kind: "imageAsset";
8034
8196
  assetId: string;
@@ -8037,6 +8199,19 @@ type BlueprintSoundHandle = {
8037
8199
  kind: "soundHandle";
8038
8200
  id: string;
8039
8201
  };
8202
+ type BlueprintCharacterInfo = {
8203
+ id: string;
8204
+ /** Author-facing display name. Empty when the character is unnamed - never falls back to `id`, which is a UUID. */
8205
+ name: string;
8206
+ /**
8207
+ * The author's accent colour, already in pin shape. Null when the character has no colour set,
8208
+ * which is a different thing from "white" - see {@link blueprintCharacterColorOrDefault} for
8209
+ * what a non-nullable colour pin does with it.
8210
+ */
8211
+ color: BlueprintRGBAColor | null;
8212
+ /** The character's default dialog avatar, or null when it has none. */
8213
+ avatar: BlueprintImageAsset | null;
8214
+ };
8040
8215
  type AppearanceSystemCondition = Partial<{
8041
8216
  hovered: boolean;
8042
8217
  active: boolean;
@@ -8400,6 +8575,18 @@ type BlueprintHostApiRuntime = {
8400
8575
  * character is currently wearing - the engine resolves it off the live portrait element.
8401
8576
  */
8402
8577
  getSpeakerAvatar: () => BlueprintImageAsset | null;
8578
+ /**
8579
+ * The speaking character's authored accent colour, already in pin shape. Opaque white when
8580
+ * nobody is speaking, the narrator is, or the character has no colour - the pin it feeds is
8581
+ * a non-nullable RGBAColor, so "no colour" and "the default colour" are the same answer.
8582
+ */
8583
+ getSpeakerColor: () => BlueprintRGBAColor;
8584
+ /**
8585
+ * Any character by id, from the table mirrored into global state - the addressable read the
8586
+ * speaker-scoped getters above cannot do. Null when the id is empty, or names a character
8587
+ * that is not (or is no longer) in the project.
8588
+ */
8589
+ getCharacter: (characterId: string) => BlueprintCharacterInfo | null;
8403
8590
  getNotifications: () => BlueprintGameNotification[];
8404
8591
  getChoiceCount: () => number;
8405
8592
  isNvlMode: () => boolean;
@@ -8428,6 +8615,37 @@ type BlueprintHostApiRuntime = {
8428
8615
  /** Milliseconds from the start of the file, not from the clip's in point. */
8429
8616
  seek: (handle: BlueprintSoundHandle, timeMs: number) => Promise<void>;
8430
8617
  isPlaying: (handle: BlueprintSoundHandle) => boolean;
8618
+ /**
8619
+ * The volume a **host-owned** media element (the `nl.video` widget's `<video>`) must be set
8620
+ * to so it obeys the same mixer everything else does.
8621
+ *
8622
+ * A DOM element the host created sits on none of the engine's gain nodes, so the whole
8623
+ * product has to be computed and written to `element.volume`: the authored volume, times
8624
+ * every bus in the track's chain, times the player's slider for whichever seeded bus that
8625
+ * chain runs through, times master. Without this a muted game keeps playing video at full
8626
+ * volume. Pair it with {@link subscribeMixerChanges} - a value read once goes stale the
8627
+ * moment the player drags a slider, bus faders included.
8628
+ */
8629
+ resolveElementVolume: (input: {
8630
+ audioTrackId?: string | null;
8631
+ volume?: number | null;
8632
+ }) => number;
8633
+ /** Fires when any preference feeding `resolveElementVolume` changes. Returns a disposer. */
8634
+ subscribeMixerChanges: (listener: () => void) => () => void;
8635
+ /**
8636
+ * An audio **track**'s own volume, 0..1 - one strip of the player's mixer.
8637
+ *
8638
+ * The player-facing counterpart of {@link setVolume}, which addresses one playing clip by
8639
+ * handle. A track is a bus every clip beneath it is routed through, so setting one applies
8640
+ * live to everything already playing and survives the clip that provoked it. This is what
8641
+ * makes "turn Alice down" expressible at all: the four fixed volume preferences can only
8642
+ * reach the three buses the engine seeds.
8643
+ *
8644
+ * An unknown or deleted track id reads as unity and writes nowhere, so a settings page
8645
+ * built against a track the author later removed degrades to an inert slider.
8646
+ */
8647
+ getTrackVolume: (trackId: string) => number;
8648
+ setTrackVolume: (trackId: string, volume: number) => Promise<void>;
8431
8649
  };
8432
8650
  devtools: {
8433
8651
  log: (level: string, message: string) => void;
@@ -8441,9 +8659,25 @@ declare const BLUEPRINT_SOUND_CHANNELS: readonly [
8441
8659
  type BlueprintSoundChannel = (typeof BLUEPRINT_SOUND_CHANNELS)[number];
8442
8660
  type BlueprintSoundPlayInput = {
8443
8661
  assetId: string;
8444
- channel: BlueprintSoundChannel;
8445
- loop: boolean;
8446
- volume: number;
8662
+ /**
8663
+ * Project audio track (`ProjectAudioTrack.id`), which **is** the engine bus this clip is routed
8664
+ * into, and whose own gain is applied live by the gain graph rather than folded in here. It also
8665
+ * supplies the loop default below. Absent resolves to the seeded SFX bus, which is what an
8666
+ * unqualified "play this clip" has always meant.
8667
+ */
8668
+ audioTrackId?: string | null;
8669
+ /** Author override; absent means the track's own default. */
8670
+ loop?: boolean | null;
8671
+ /** Author override, 0..1 as authored; absent means unity. Never pre-multiplied by a bus gain. */
8672
+ volume?: number | null;
8673
+ /** Fade-in in milliseconds; absent means a hard start (a fade belongs to the moment). */
8674
+ fadeInMs?: number | null;
8675
+ /**
8676
+ * The pre-track channel select, kept only so a graph or a host call written before tracks
8677
+ * existed still plays on the bus it named. Read only when `audioTrackId` is unset, and mapped
8678
+ * to that channel's built-in track by the transport. Nothing writes it any more.
8679
+ */
8680
+ channel?: BlueprintSoundChannel;
8447
8681
  };
8448
8682
  type GameLocalizationConfigSnapshot = Pick<GameLocalizationBundle, "sourceLocale" | "locales"> & Partial<Pick<GameLocalizationBundle, "tables" | "keys">>;
8449
8683
  type BlueprintGameNotification = {
@@ -9884,6 +10118,8 @@ declare enum Services {
9884
10118
  ProjectStats = "projectStats",
9885
10119
  /** Project-level persistent variable registry (blueprint-declared persistent vars); M-VAR */
9886
10120
  VariableRegistry = "variableRegistry",
10121
+ /** Project-level audio tracks: the authoring-time mix presets every audio surface points at */
10122
+ AudioTracks = "audioTracks",
9887
10123
  /** Aggregate "is my work on disk?" state: auto-saver states + the table of files that failed */
9888
10124
  SaveStatus = "saveStatus",
9889
10125
  Localization = "localization",
@@ -10799,6 +11035,14 @@ export interface SelectProps {
10799
11035
  menuDataAttributes?: Record<`data-${string}`, string | undefined>;
10800
11036
  /** Optional z-index override for the dropdown menu panel. */
10801
11037
  menuZIndex?: number;
11038
+ /**
11039
+ * Accessible name for the trigger.
11040
+ *
11041
+ * The trigger's own text is the *selected value*, not what the value means, so a select whose
11042
+ * visible label lives in a row header beside it reaches assistive tech announcing "Voice" with
11043
+ * no clue that the question was "routes into". Pass the row's title.
11044
+ */
11045
+ ariaLabel?: string;
10802
11046
  /**
10803
11047
  * This select changes what is SHOWN and writes nothing.
10804
11048
  *
@@ -10808,8 +11052,22 @@ export interface SelectProps {
10808
11052
  * getting it wrong offers a write inside a frozen project.
10809
11053
  */
10810
11054
  inspectOnly?: boolean;
11055
+ /**
11056
+ * This select normally writes, but right now it may only be read.
11057
+ *
11058
+ * **Open, not shut.** `disabled` is the obvious treatment and it is the wrong one here: a
11059
+ * dropdown that cannot be opened hides its option list, and on a frozen project that list is
11060
+ * project data the author came to look at - which keys exist, which states this element has,
11061
+ * what the choices even were. So the trigger still opens (through {@link InspectOnlyButton}, so
11062
+ * an ancestor clamp cannot reach it either), the menu still renders with the current value
11063
+ * marked, and every row is inert.
11064
+ *
11065
+ * Different from {@link inspectOnly}, which is for a select whose `onChange` never writes at
11066
+ * all and whose rows therefore stay live.
11067
+ */
11068
+ readOnly?: boolean;
10811
11069
  }
10812
- declare function Select({ options, value, onChange, placeholder, disabled, size, variant, fullWidth, className, multiple, portalMenu, menuPlacement, menuClassName, menuDataAttributes, menuZIndex, inspectOnly, }: SelectProps): import("react/jsx-runtime").JSX.Element;
11070
+ declare function Select({ options, value, onChange, placeholder, disabled, size, variant, fullWidth, className, multiple, portalMenu, menuPlacement, menuClassName, menuDataAttributes, menuZIndex, inspectOnly, readOnly, ariaLabel, }: SelectProps): import("react/jsx-runtime").JSX.Element;
10813
11071
  declare function Combobox({ options, value, onChange, placeholder, disabled, size, variant, fullWidth, className, filterOptions, }: SelectProps & {
10814
11072
  filterOptions?: boolean;
10815
11073
  }): import("react/jsx-runtime").JSX.Element;
@@ -10829,6 +11087,90 @@ export interface SwitchProps extends Omit<React$1.ButtonHTMLAttributes<HTMLButto
10829
11087
  loading?: boolean;
10830
11088
  }
10831
11089
  declare function Switch({ checked, onCheckedChange, size, variant, disabled, loading, className, onClick, ...props }: SwitchProps): import("react/jsx-runtime").JSX.Element;
11090
+ /**
11091
+ * What a control inside an editor renders as while the workspace is frozen.
11092
+ *
11093
+ * The correctness half of freeze is not here and never will be: `@/lib/app/writeFreeze` refuses every
11094
+ * write of project data at the boundary, so nothing a panel does while frozen reaches the author's
11095
+ * files. This module is the other half - **do not offer an action that cannot happen.** Measured
11096
+ * before it existed: a frozen workspace let the author walk the whole New Scene flow, name the scene,
11097
+ * press OK, and get a tab for a scene that was never written. The app said yes and then discarded it.
11098
+ *
11099
+ * It follows `components/ui/freezeActionPolicy` (the same decision, for the top bar) on all four
11100
+ * counts, because a workspace where the top bar and the panels disagree about what a freeze looks like
11101
+ * teaches the author nothing:
11102
+ *
11103
+ * - **Disabled, not hidden.** A control that vanishes cannot explain itself; the author reads the gap
11104
+ * as a broken editor rather than as a frozen project. So {@link FreezeGuard.writes} greys a button
11105
+ * and puts the reason on it, and never removes it.
11106
+ * - **One reason, one string.** `workspace.shell.freeze.unavailable` is the same text the top bar
11107
+ * uses. The author learns "this is what frozen looks like" once instead of reading a different
11108
+ * excuse per panel.
11109
+ * - **Navigation, selection and inspection are never touched.** Opening a tab, selecting an element,
11110
+ * reading its properties, expanding a tree, scrolling a timeline: all of that is the entire point of
11111
+ * a frozen workspace, so nothing here has any business disabling it. Only the write is blocked.
11112
+ * - **Computed at render, never written back.** Documents, registries and registered menu items
11113
+ * outlive a freeze; a freeze that marked them disabled would leave them disabled after the thaw.
11114
+ *
11115
+ * Two shapes, because the surfaces genuinely differ. A button has somewhere to put a reason;
11116
+ * a drag gesture does not, and {@link FreezeGuard.gesture} makes it inert instead - a half-inert
11117
+ * gesture (drag starts, drop does nothing) is worse than either, because it reads as a bug.
11118
+ */
11119
+ export type FrozenControlProps = {
11120
+ disabled: boolean;
11121
+ /** Hover text: the freeze reason when the freeze is why, the caller's own otherwise. */
11122
+ title: string | undefined;
11123
+ };
11124
+ export type FreezeGuard = {
11125
+ /** Whether this window's project data is frozen right now. */
11126
+ readonly frozen: boolean;
11127
+ /**
11128
+ * The single hover string for everything the freeze switches off.
11129
+ *
11130
+ * Exposed for the call sites that build their own `title` (a control that already composes one,
11131
+ * a tooltip component); prefer {@link writes}, which picks between this and the caller's own.
11132
+ */
11133
+ readonly reason: string;
11134
+ /**
11135
+ * Render props for a control whose action writes project data.
11136
+ *
11137
+ * `ownDisabled` / `ownTitle` are the control's existing state, kept intact: a button that was
11138
+ * already disabled for its own reason must not start claiming the freeze is why, because that
11139
+ * lie outlives the thaw.
11140
+ */
11141
+ writes(ownDisabled?: boolean, ownTitle?: string): FrozenControlProps;
11142
+ /**
11143
+ * The render fields for a menu row that writes project data.
11144
+ *
11145
+ * The row still renders, so the author can see the action exists - the same bargain the top bar's
11146
+ * menus make - and `tooltip` says why it is off. That field only exists because this pass added it
11147
+ * to `ContextMenuItemDef`; before it, a greyed row carried no reason at all, and the explanation
11148
+ * could not go in the label without turning a disabled menu into a paragraph.
11149
+ */
11150
+ menuRow(ownDisabled?: boolean): {
11151
+ disabled: boolean;
11152
+ tooltip: string | undefined;
11153
+ };
11154
+ /**
11155
+ * A drag/pointer handler, or `undefined` while frozen - so the gesture is never half-attached.
11156
+ *
11157
+ * Returning `undefined` rather than a no-op is deliberate: `draggable`, `onDragOver` and React
11158
+ * Flow's `nodesDraggable` all key off whether a handler is there, and a present-but-inert
11159
+ * `onDragStart` produces a drag that picks up and refuses to drop. Pass the whole set through
11160
+ * this, or none of it.
11161
+ */
11162
+ gesture<F>(handler: F): F | undefined;
11163
+ /**
11164
+ * A handler that has no control to grey out - a keybinding, a dialog's confirm, a drop target -
11165
+ * wrapped so it does nothing while frozen.
11166
+ *
11167
+ * The author is not told here. They will be: the write boundary announces the refusal through
11168
+ * `SaveStatusService` the moment anything tries to save, and a toast per blocked keystroke would
11169
+ * bury that one useful message under a dozen.
11170
+ */
11171
+ run<A extends unknown[], R>(handler: (...args: A) => R): (...args: A) => R | undefined;
11172
+ };
11173
+ declare function useFreezeGuard(): FreezeGuard;
10832
11174
  type DivProps = React$1.HTMLAttributes<HTMLDivElement>;
10833
11175
  declare function PluginAssetSelector(props: AssetSelectorProps): import("react/jsx-runtime").JSX.Element | null;
10834
11176
  export type PluginPanelRootProps = DivProps & {
@@ -10862,6 +11204,20 @@ export type PluginPanelEmptyStateProps = DivProps & {
10862
11204
  };
10863
11205
  declare function PluginPanelEmptyState({ icon, title, description, actions, className, ...props }: PluginPanelEmptyStateProps): import("react/jsx-runtime").JSX.Element;
10864
11206
  declare const pluginUi: Readonly<{
11207
+ /**
11208
+ * What a plugin's writing controls render as while the project is read-only.
11209
+ *
11210
+ * Studio's own panels call this, and a plugin surface calling it renders the identical
11211
+ * treatment: greyed rather than hidden, one shared reason on hover, and navigation, selection and
11212
+ * inspection left completely alone - a frozen project is one the author came to *read*. Route
11213
+ * every control that writes through `writes()`, every drag through `gesture()`, and every
11214
+ * keybinding or dialog confirm through `run()`.
11215
+ *
11216
+ * Affordance only. The refusal itself happens at the write boundary whether a plugin opts in or
11217
+ * not; what opting in buys is not lying to the author about what will happen. See
11218
+ * `services.workspace` for the non-React half.
11219
+ */
11220
+ useFreezeGuard: typeof useFreezeGuard;
10865
11221
  Button: typeof Button;
10866
11222
  IconButton: typeof IconButton;
10867
11223
  Input: typeof Input;
@@ -10934,6 +11290,7 @@ export type PluginBlueprintNodeDef = Omit<BlueprintNodeDef, "execute"> & {
10934
11290
  */
10935
11291
  export type PluginBlueprintNodeContext = RuntimeBlueprintNodeContext;
10936
11292
  export declare const ui: Readonly<{
11293
+ useFreezeGuard: typeof useFreezeGuard;
10937
11294
  Button: typeof Button;
10938
11295
  IconButton: typeof IconButton;
10939
11296
  Input: typeof Input;
@@ -11038,10 +11395,59 @@ export type PluginTextEditorService = {
11038
11395
  registerPreview(def: PluginTextEditorPreviewDef): PluginCleanup;
11039
11396
  registerAction(def: PluginTextEditorActionDef): PluginCleanup;
11040
11397
  };
11398
+ /**
11399
+ * Anything a plugin writes through {@link PluginStorageService} is **project content**: it lands in
11400
+ * `editor/services/`, inside the versioned working tree, alongside the story and the assets. Two
11401
+ * consequences follow, and this service is how a plugin answers both of them.
11402
+ */
11041
11403
  export type PluginStorageService = {
11042
11404
  readJson<T extends Record<string, any>>(namespace: string): Promise<T | null>;
11405
+ /**
11406
+ * Writes are **silently discarded while the project is frozen** ({@link PluginWorkspaceService}).
11407
+ * That is deliberate at the boundary - it is what keeps a version the author is only *looking* at
11408
+ * from being written over - but it means a plugin that mutates its own memory first and writes
11409
+ * second ends up with memory the disk does not have. Check `workspace.frozen` before you mutate,
11410
+ * not after.
11411
+ */
11043
11412
  writeJson<T extends Record<string, any>>(namespace: string, data: T): Promise<void>;
11044
11413
  };
11414
+ /** Why project data is currently read-only: an older version is on screen, or the author froze it. */
11415
+ export type PluginFreezeReason = "revision" | "manual";
11416
+ /**
11417
+ * The state of the project a plugin's data lives in.
11418
+ *
11419
+ * Studio's version control moves the whole working tree under the editors' feet - restoring a past
11420
+ * version, or displaying one read-only while the author browses history - and a plugin store is part
11421
+ * of that tree. So there are exactly two things a plugin has to do to be version-controlled, and both
11422
+ * are here:
11423
+ *
11424
+ * - **Do not offer a write that cannot happen.** While `frozen`, project writes are refused at the
11425
+ * boundary; a plugin that keeps its buttons live lets the author edit and then throws the edit
11426
+ * away. Grey them out (`ui.useFreezeGuard()` renders exactly what Studio's own panels render) and
11427
+ * make every mutation bail before it touches memory.
11428
+ * - **Re-read when Studio does.** {@link registerReloader} enrols your store in the pass Studio runs
11429
+ * after a restore, a thaw, or entering a version view. Skipping it is not a cosmetic staleness
11430
+ * bug: your pre-restore memory is still in RAM, and the author's next edit writes it back over the
11431
+ * version they just restored.
11432
+ */
11433
+ export type PluginWorkspaceService = {
11434
+ /** Whether project data may be written right now. Read at call time; never cached. */
11435
+ readonly frozen: boolean;
11436
+ /** Why, or `null` when the project is writable. */
11437
+ readonly freezeReason: PluginFreezeReason | null;
11438
+ /** Fires on every change, with the new state. */
11439
+ onFreezeChange(listener: (frozen: boolean, reason: PluginFreezeReason | null) => void): PluginCleanup;
11440
+ /**
11441
+ * Re-read everything this plugin holds in memory, because the project's documents have been
11442
+ * replaced.
11443
+ *
11444
+ * Called with project writes held off and the new version already installed at the read
11445
+ * boundary, so a plain `storage.readJson` inside it returns the right bytes. Read before you drop
11446
+ * what you have: throwing must leave the plugin with its old data rather than with half of a
11447
+ * document. Register once per store, at `setup`.
11448
+ */
11449
+ registerReloader(reload: () => Promise<void> | void): PluginCleanup;
11450
+ };
11045
11451
  export type PluginAssetsService = {
11046
11452
  getMap(): AssetsMap;
11047
11453
  list<T extends AssetType>(type: T): Asset<T, AssetSource>[];
@@ -11125,6 +11531,8 @@ export type PluginServices = {
11125
11531
  storage: PluginStorageService;
11126
11532
  assets: PluginAssetsService;
11127
11533
  i18n: PluginI18n;
11534
+ /** The state of the project your data lives in; see {@link PluginWorkspaceService}. */
11535
+ workspace: PluginWorkspaceService;
11128
11536
  /** Extend Studio's built-in text editor; see {@link PluginTextEditorService}. */
11129
11537
  textEditor: PluginTextEditorService;
11130
11538
  ui: {
package/dist/plugin.d.ts CHANGED
@@ -35,6 +35,8 @@ export {
35
35
  ContextMenuProps,
36
36
  ContextMenuSeparatorDef,
37
37
  definePlugin,
38
+ FreezeGuard,
39
+ FrozenControlProps,
38
40
  InputSize,
39
41
  InputVariant,
40
42
  isPluginDefinition,
@@ -48,6 +50,7 @@ export {
48
50
  PluginBlueprintNodeDef,
49
51
  PluginCleanup,
50
52
  PluginDefinition,
53
+ PluginFreezeReason,
51
54
  PluginI18n,
52
55
  PluginIdentity,
53
56
  PluginInstallPermission,
@@ -82,6 +85,7 @@ export {
82
85
  PluginTranslator,
83
86
  PluginUiKit,
84
87
  PluginVoiceUnitEntry,
88
+ PluginWorkspaceService,
85
89
  ProgressProps,
86
90
  ProgressSize,
87
91
  ProgressVariant,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "narraleaf-studio",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "TypeScript declarations for the NarraLeaf Studio plugin API (narraleaf-studio/plugin and narraleaf-studio/runtime).",
5
5
  "license": "MPL-2.0",
6
6
  "type": "module",