@scm-js/plugin-api 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.d.ts +235 -3
  2. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -2745,6 +2745,16 @@ export interface ViewApi {
2745
2745
  /** Grid spacing in map pixels (32 = one tile). */
2746
2746
  gridSize(): number;
2747
2747
  setGridSize(size: 8 | 16 | 32 | 64 | 128): void;
2748
+ /**
2749
+ * Highlight something on the map for a moment: the rect an edit filled, the units it
2750
+ * placed, the location it moved. Fades by itself; several may run at once. Nothing to
2751
+ * clean up.
2752
+ *
2753
+ * @example
2754
+ * const r = api.document.edit("Fill", (tx) => tx.fillFlat(rect, terrain));
2755
+ * if (r.changed) api.view.flash({ rect });
2756
+ */
2757
+ flash(target: FlashTarget): void;
2748
2758
  }
2749
2759
  export interface CommandSpec {
2750
2760
  /**
@@ -3447,6 +3457,13 @@ export interface DialogHandle {
3447
3457
  isOpen(): boolean;
3448
3458
  /** Change the title strip's text. */
3449
3459
  setTitle(title: string): void;
3460
+ /**
3461
+ * Say the dialog is working: a ring and `label` at the left of the footer, and every
3462
+ * footer button disabled until `setBusy(false)`. A button's own `run` already does this
3463
+ * for as long as its promise is pending — this is for work the buttons did not start,
3464
+ * such as the first load, or a search that runs as the user types.
3465
+ */
3466
+ setBusy(label: string | false): void;
3450
3467
  }
3451
3468
  /**
3452
3469
  * A panel floats over the map and does not block it: the user keeps drawing, scrolling
@@ -3455,8 +3472,19 @@ export interface DialogHandle {
3455
3472
  */
3456
3473
  export interface PanelSpec {
3457
3474
  title: string;
3458
- /** In CSS pixels; 260 by default. The panel is as tall as its content, up to the map's height. */
3475
+ /** In CSS pixels; 260 by default. The panel is as tall as its content, up to the map's height. Ignored when docked. */
3459
3476
  width?: number;
3477
+ /**
3478
+ * Where the panel lives. `"float"` (the default) is a frame over the map the user drags
3479
+ * about. `"right"` puts it in the right dock as a panel of its own, under Minimap, Layers
3480
+ * and Properties, with the same head and hide button the built-in panels have — the
3481
+ * choice for anything the user keeps open while working, such as an assistant or a
3482
+ * readout. A docked panel shows the dock even when the user has hidden every built-in
3483
+ * panel in it.
3484
+ */
3485
+ dock?: "float" | "right";
3486
+ /** A docked panel that takes the dock's spare height (a transcript, a long list); off by default. */
3487
+ grow?: boolean;
3460
3488
  /** Fill `body` (an empty `<div>` inside the panel); return a cleanup if you need one. */
3461
3489
  mount(body: HTMLElement, panel: PanelHandle): void | (() => void);
3462
3490
  /** The panel closed, whichever way. */
@@ -3467,6 +3495,85 @@ export interface PanelHandle {
3467
3495
  isOpen(): boolean;
3468
3496
  setTitle(title: string): void;
3469
3497
  }
3498
+ /**
3499
+ * One cell in the status bar (`ui.statusItem`): a line of text with the plugin's icon,
3500
+ * a spinner while `busy`, and a click. It is how a plugin that works in the background
3501
+ * stays visible without a panel — "AI · working 12 s", "3 problems", "Synced".
3502
+ */
3503
+ export interface StatusItemSpec {
3504
+ text: string;
3505
+ /** The tooltip. */
3506
+ title?: string;
3507
+ /** Show a spinner in the cell. */
3508
+ busy?: boolean;
3509
+ /** Paint the cell as a warning. */
3510
+ warn?: boolean;
3511
+ onClick?: () => void;
3512
+ }
3513
+ export interface StatusItemHandle {
3514
+ /** Change any of the fields; the cell redraws. */
3515
+ set(patch: Partial<StatusItemSpec>): void;
3516
+ remove(): void;
3517
+ isShown(): boolean;
3518
+ }
3519
+ /**
3520
+ * The dialogs a plugin may add to (`ui.dialogSlot`), and the fields each one exposes to
3521
+ * the slot. A field is the dialog's *working copy* — what the person sees in the form,
3522
+ * not yet applied to the map — so a slot's button can fill it in and leave OK to them.
3523
+ *
3524
+ * - `mapProperties`: `name`, `description`.
3525
+ * - `textTriggerEditor`: `text` (the whole editor); `payload.briefing` says which list.
3526
+ * - `triggerEditor`, `stringEditor`, `playerSettings`, `missionBriefing`: no fields, a slot only.
3527
+ */
3528
+ export type SlottedDialogId = "mapProperties" | "textTriggerEditor" | "triggerEditor" | "stringEditor" | "playerSettings" | "missionBriefing";
3529
+ /** A form field a dialog lends to a slot: read it, set it, hear it change. */
3530
+ export interface DialogField {
3531
+ get(): string;
3532
+ set(value: string): void;
3533
+ }
3534
+ /** What a slot's `mount` is handed: which dialog, its payload, its fields, and a way to close it. */
3535
+ export interface DialogSlotHost {
3536
+ readonly dialog: SlottedDialogId;
3537
+ readonly payload: Readonly<Record<string, unknown>>;
3538
+ readonly fields: Readonly<Record<string, DialogField>>;
3539
+ close(): void;
3540
+ }
3541
+ /**
3542
+ * Something a plugin mounts *inside* a built-in dialog — a button in Map Properties
3543
+ * that suggests a name, one in the Text Trigger Editor that explains the text. The slot
3544
+ * is a row at the left of the dialog's footer; fill it with the widgets, and it looks like
3545
+ * part of the dialog. `mount` runs every time the dialog opens, with a cleanup on close.
3546
+ */
3547
+ export interface DialogSlotSpec {
3548
+ mount(body: HTMLElement, host: DialogSlotHost): void | (() => void);
3549
+ }
3550
+ /**
3551
+ * What `view.flash` highlights: a tile rect, or units / locations by index. The
3552
+ * highlight fades over `ms` (600 by default) and never blocks or takes the pointer; it is
3553
+ * the shared way to say "this just changed" or "look here", so every plugin's flash looks
3554
+ * the same. `kind` picks the colour: `"change"` (gold, the default) or `"attention"` (teal).
3555
+ */
3556
+ export type FlashTarget = {
3557
+ rect: Rect;
3558
+ kind?: FlashKind;
3559
+ ms?: number;
3560
+ } | {
3561
+ units: number[];
3562
+ kind?: FlashKind;
3563
+ ms?: number;
3564
+ } | {
3565
+ locations: number[];
3566
+ kind?: FlashKind;
3567
+ ms?: number;
3568
+ } | {
3569
+ tiles: {
3570
+ x: number;
3571
+ y: number;
3572
+ }[];
3573
+ kind?: FlashKind;
3574
+ ms?: number;
3575
+ };
3576
+ export type FlashKind = "change" | "attention";
3470
3577
  /** The pointer over the map, in the map's own units. A tile is 32 × 32 pixels. */
3471
3578
  export interface MapPointer {
3472
3579
  /** Map pixels. Kept inside the map while a button is held, as the built-in brushes do. */
@@ -3610,8 +3717,33 @@ export interface UiApi {
3610
3717
  fileName: string;
3611
3718
  } | null>;
3612
3719
  dialog(spec: DialogSpec): DialogHandle;
3613
- /** Open a floating panel over the map. As many as you like; each closes with the plugin. */
3720
+ /**
3721
+ * Open a panel: floating over the map, or docked at the right with the built-in panels
3722
+ * (`spec.dock`). As many as you like; each closes with the plugin.
3723
+ */
3614
3724
  panel(spec: PanelSpec): PanelHandle;
3725
+ /**
3726
+ * A cell of your own in the status bar. One line of text beside the plugin's icon, a
3727
+ * spinner while `busy`, a click. Keep the handle and `set` it as things move; it
3728
+ * leaves with `remove()` or the plugin.
3729
+ */
3730
+ statusItem(spec: StatusItemSpec): StatusItemHandle;
3731
+ /**
3732
+ * Add to a built-in dialog. `mount` runs in a row at the left of the dialog's footer each
3733
+ * time that dialog opens, with the dialog's working-copy fields to read and fill
3734
+ * (`SlottedDialogId` lists which dialogs and fields). Returns the registration; it
3735
+ * leaves with `dispose()` or the plugin.
3736
+ *
3737
+ * @example
3738
+ * api.ui.dialogSlot("mapProperties", {
3739
+ * mount(body, dlg) {
3740
+ * body.append(api.ui.widgets.button("Suggest a name", { onClick: async () => {
3741
+ * dlg.fields.name.set(await suggestName(dlg.fields.description.get()));
3742
+ * } }));
3743
+ * },
3744
+ * });
3745
+ */
3746
+ dialogSlot(dialog: SlottedDialogId, spec: DialogSlotSpec): Disposable;
3615
3747
  /**
3616
3748
  * Take over the pointer on the map until `stop()`, Esc, a right-click, a map change
3617
3749
  * or another tool. One tool runs at a time — starting one stops the previous — and a
@@ -3719,8 +3851,18 @@ export interface ButtonOptions extends WidgetOptions {
3719
3851
  primary?: boolean;
3720
3852
  danger?: boolean;
3721
3853
  ghost?: boolean;
3854
+ /** Start off waiting: a ring in front of the label, and the button disabled. */
3855
+ busy?: boolean;
3722
3856
  onClick?: (event: MouseEvent) => void;
3723
3857
  }
3858
+ /** The `<button>`, with the one call that puts it in and out of its waiting state. */
3859
+ export type ButtonElement = HTMLButtonElement & {
3860
+ /**
3861
+ * A ring in front of the label while `busy`, and the button disabled — so the press
3862
+ * that started the work cannot be repeated, and says why it cannot.
3863
+ */
3864
+ setBusy(busy: boolean): void;
3865
+ };
3724
3866
  export interface CheckboxOptions extends WidgetOptions {
3725
3867
  value?: boolean;
3726
3868
  /** A radio button instead; give the group a `name`. */
@@ -3775,13 +3917,80 @@ export interface ListOptions<T> {
3775
3917
  className?: string;
3776
3918
  onPick?: (value: T, index: number) => void;
3777
3919
  }
3920
+ export interface SpinnerOptions extends WidgetOptions {
3921
+ /** `"sm"` 10px, `"md"` 14px (the default), `"lg"` 20px. */
3922
+ size?: "sm" | "md" | "lg";
3923
+ /** Text beside the ring. Without it you get the bare ring, to put beside your own. */
3924
+ label?: string;
3925
+ }
3926
+ export interface ProgressBarOptions extends WidgetOptions {
3927
+ /** How far along to start: 0…1, or null (the default) for the sliding bar that means "no idea how long". */
3928
+ value?: number | null;
3929
+ /** A line under the bar — what is being waited for, or how much of it is done. */
3930
+ label?: string;
3931
+ /** The percentage at the end of the bar; on for a bar that knows how far along it is. */
3932
+ percent?: boolean;
3933
+ /** Width in CSS pixels; it fills the width it is given by default. */
3934
+ width?: number;
3935
+ }
3936
+ export type ProgressBarElement = HTMLElement & {
3937
+ /**
3938
+ * How far along: 0…1, or null for the sliding bar. `label` replaces the line under it.
3939
+ * Cheap to call often — it repaints only when the bar actually moves.
3940
+ */
3941
+ set(value: number | null, label?: string): void;
3942
+ };
3943
+ export interface StatusLineOptions extends WidgetOptions {
3944
+ /** What it says before anything has happened. */
3945
+ text?: string;
3946
+ }
3947
+ /**
3948
+ * The line along the bottom of a dialog: what happened, what is happening, how far
3949
+ * along it is, and the Cancel that stops it — in one place, so a dialog has one voice.
3950
+ * It is a live region, so a screen reader hears the outcome (but not every byte of a
3951
+ * download: the bar's own movement is not announced).
3952
+ */
3953
+ export type StatusLineElement = HTMLElement & {
3954
+ /** A finished line. `kind` colours it; nothing means plain. */
3955
+ set(text: string, kind?: "ok" | "warn" | "error"): void;
3956
+ /** Waiting, with no idea how long: a ring and the text. */
3957
+ busy(text: string): void;
3958
+ /** Waiting with a share done (0…1), or null for the sliding bar. */
3959
+ progress(text: string, value: number | null): void;
3960
+ /** A Cancel beside the line that calls `stop`; null takes it away. */
3961
+ cancel(stop: (() => void) | null): void;
3962
+ /** Back to an empty line. */
3963
+ clear(): void;
3964
+ };
3965
+ export interface SkeletonOptions extends WidgetOptions {
3966
+ /** CSS width, `"100%"` by default. */
3967
+ width?: string;
3968
+ /** Height in CSS pixels: 10 for a line, 44 for a block. */
3969
+ height?: number;
3970
+ /** How many lines, stacked; 1 by default. The last is drawn short, as text ends short. */
3971
+ lines?: number;
3972
+ /** A block — the place a thumbnail or a picture will take — rather than a line of text. */
3973
+ block?: boolean;
3974
+ }
3975
+ export interface BusyOptions {
3976
+ /** What is being waited for, over the covered box. */
3977
+ label?: string;
3978
+ /** Dim what is already there rather than leaving it at full strength; true by default. */
3979
+ dim?: boolean;
3980
+ }
3981
+ export interface BusyHandle {
3982
+ /** Change the label without lifting the cover. */
3983
+ set(label: string): void;
3984
+ /** Uncover the box. Safe to call twice. */
3985
+ done(): void;
3986
+ }
3778
3987
  /**
3779
3988
  * Buttons, fields, forms and lists in the editor's own styles, as plain DOM. A plugin
3780
3989
  * dialog built from these looks like a built-in one; `el` is the escape hatch for
3781
3990
  * anything they do not cover.
3782
3991
  */
3783
3992
  export interface WidgetsApi {
3784
- button(label: string, options?: ButtonOptions): HTMLButtonElement;
3993
+ button(label: string, options?: ButtonOptions): ButtonElement;
3785
3994
  checkbox(label: string, options?: CheckboxOptions): CheckboxElement;
3786
3995
  text(options?: TextFieldOptions): HTMLInputElement;
3787
3996
  number(options?: NumberFieldOptions): HTMLInputElement;
@@ -3796,6 +4005,29 @@ export interface WidgetsApi {
3796
4005
  hint(text: string): HTMLElement;
3797
4006
  separator(): HTMLElement;
3798
4007
  list<T>(items: ListItem<T>[], options?: ListOptions<T>): HTMLElement;
4008
+ /** A turning ring: on its own to put beside your own text, or with a `label` beside it. */
4009
+ spinner(options?: SpinnerOptions): HTMLElement;
4010
+ /**
4011
+ * A bar, with the percentage and a line under it. `set(0…1)` moves it; `set(null)`
4012
+ * gives the sliding bar for work whose length is not known. Use it where the wait has
4013
+ * a size — a download, a pass over every trigger — and a spinner where it does not.
4014
+ */
4015
+ progressBar(options?: ProgressBarOptions): ProgressBarElement;
4016
+ /** The dialog's bottom line: `set`, `busy`, `progress` and a `cancel` button. */
4017
+ statusLine(options?: StatusLineOptions): StatusLineElement;
4018
+ /**
4019
+ * A grey stand-in for content that has not arrived, in the shape it will take. Rows of
4020
+ * these in the list you are about to fill say more than an empty box does, and the list
4021
+ * does not jump when the answer lands.
4022
+ */
4023
+ skeleton(options?: SkeletonOptions): HTMLElement;
4024
+ /**
4025
+ * Cover a box while what is in it is being replaced: dimmed, deaf to clicks, with a
4026
+ * ring and a label over it. `done()` uncovers it. Give it the box, not the page — the
4027
+ * search field that starts the work should stay live so the user can change their mind.
4028
+ * Calling it again on a covered box only changes the label.
4029
+ */
4030
+ busy(target: HTMLElement, options?: BusyOptions | string): BusyHandle;
3799
4031
  }
3800
4032
  export type TopMenu = "File" | "Edit" | "View" | "Layer" | "Scenario" | "Triggers" | "Tools" | "Plugins" | "Help";
3801
4033
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scm-js/plugin-api",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Type declarations for the scmJS plugin API",
5
5
  "license": "MIT",
6
6
  "types": "index.d.ts",