@ham2k/extension-sdk 0.9.0 → 0.10.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/AGENTS.md +1 -1
- package/dist/index.d.ts +186 -17
- package/dist/index.js +9 -1
- package/dist/nearby.js +10 -0
- package/dist/referenceActivity.js +4 -3
- package/docs/distribution.md +17 -12
- package/docs/forms.md +17 -0
- package/docs/hooks.md +246 -18
- package/package.json +1 -1
- package/samples/README.md +1 -1
- package/samples/{k2hrc-svg-scenes → k2hrc-panel-scenes}/README.md +5 -5
- package/samples/{k2hrc-svg-scenes → k2hrc-panel-scenes}/manifest.json +5 -5
- package/samples/{k2hrc-svg-scenes → k2hrc-panel-scenes}/src/scenes.ts +123 -5
- /package/dist/{svgScene.js → panelScene.js} +0 -0
- /package/samples/{k2hrc-svg-scenes → k2hrc-panel-scenes}/build.mjs +0 -0
- /package/samples/{k2hrc-svg-scenes → k2hrc-panel-scenes}/src/index.ts +0 -0
package/AGENTS.md
CHANGED
|
@@ -81,7 +81,7 @@ will actually call.
|
|
|
81
81
|
|
|
82
82
|
| Doing this | Read |
|
|
83
83
|
| --- | --- |
|
|
84
|
-
| Anything, before the reference — a whole worked extension of that shape | `samples/`: `k2hrc-hamqth` (lookup + credentials), `k2hrc-llota` (award program), `k2hrc-cqww` (contest + scoring), `k2hrc-radio` (HTML panel), `k2hrc-
|
|
84
|
+
| Anything, before the reference — a whole worked extension of that shape | `samples/`: `k2hrc-hamqth` (lookup + credentials), `k2hrc-llota` (award program), `k2hrc-cqww` (contest + scoring), `k2hrc-radio` (HTML panel), `k2hrc-panel-scenes` (experimental interactive panel scene) |
|
|
85
85
|
| Any hook at all — what the category is, what it's handed, what it must return | `docs/hooks.md`, the section named for the category |
|
|
86
86
|
| A pane in a view: `getPanels`, `render`, panel content kinds | `docs/hooks.md` §`panel` |
|
|
87
87
|
| Callsign lookups, spot sources, exporters, reference types (POTA-style) | `docs/hooks.md` §`lookup`, §`spots`, §`export`, §`ref:<type>` |
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
// @ham2k/extension-sdk 0.
|
|
2
|
-
/** Experimental v1 scene contract. All coordinates are in the scene's viewBox. */
|
|
3
|
-
export interface
|
|
1
|
+
// @ham2k/extension-sdk 0.10.0
|
|
2
|
+
/** Experimental v1 panel scene contract. All coordinates are in the scene's viewBox. */
|
|
3
|
+
export interface PanelScene {
|
|
4
4
|
version: 1;
|
|
5
5
|
width: number;
|
|
6
6
|
height: number;
|
|
7
7
|
values: Record<string, number>;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/** String state for text fields and choices. A key is in `values` or here, never both. At most 256, each up to 4096 characters. */
|
|
9
|
+
strings?: Record<string, string>;
|
|
10
|
+
/** Painted in order, each over the ones before. */
|
|
11
|
+
layers: PanelSceneLayer[];
|
|
12
|
+
controls?: PanelSceneControl[];
|
|
13
|
+
/** Where rect-less native controls and the artwork go. Omitted, the artwork fills the panel. */
|
|
14
|
+
layout?: PanelSceneLayoutNode;
|
|
10
15
|
}
|
|
11
16
|
/** A clamped, linear mapping. No expressions or extension code run per frame. */
|
|
12
17
|
export interface SceneBinding {
|
|
@@ -26,7 +31,7 @@ export interface SceneBinding {
|
|
|
26
31
|
/** Optional equally spaced output samples over input; replaces the output ramp. */
|
|
27
32
|
samples?: number[];
|
|
28
33
|
}
|
|
29
|
-
export interface
|
|
34
|
+
export interface PanelSceneLayer {
|
|
30
35
|
id: string;
|
|
31
36
|
x: number;
|
|
32
37
|
y: number;
|
|
@@ -34,7 +39,12 @@ export interface SvgSceneLayer {
|
|
|
34
39
|
height: number;
|
|
35
40
|
/** Supply exactly one of svg or text. SVG must be self-contained. */
|
|
36
41
|
svg?: string;
|
|
37
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Take the clicks that land on this layer. Off by default: layers are click-through, so
|
|
44
|
+
* artwork painted over a control does not stop it working. On, it covers the controls under it.
|
|
45
|
+
*/
|
|
46
|
+
blocksPointer?: boolean;
|
|
47
|
+
/** Exactly one of literal/value. A value naming a string shows it as-is. Use text layers, not SVG <text>, for font support.
|
|
38
48
|
* Size is unscaled; the host applies OS text scaling once. */
|
|
39
49
|
text?: {
|
|
40
50
|
value?: string;
|
|
@@ -75,24 +85,77 @@ export interface SvgSceneLayer {
|
|
|
75
85
|
flicker?: boolean;
|
|
76
86
|
};
|
|
77
87
|
}
|
|
78
|
-
|
|
88
|
+
/**
|
|
89
|
+
* `button`, `slider` and `knob` are invisible hit areas over the extension's own artwork.
|
|
90
|
+
* The `native…` kinds are Material widgets, themed by the app:
|
|
91
|
+
*
|
|
92
|
+
* - `nativeButton`: sends `activate`. `variant`, `icon`, and optionally `menu`.
|
|
93
|
+
* - `nativeSlider`: a number, between `min` and `max` in `step`s.
|
|
94
|
+
* - `nativeTextField`: a string. Commits on Enter, or on leaving the field after typing.
|
|
95
|
+
* - `nativeDropdown`, `nativeRadio`, `nativeSegmented`: a string, one of `options`.
|
|
96
|
+
* A `multi` segmented control holds its chosen values joined with commas, in option order.
|
|
97
|
+
* - `nativeSwitch`, `nativeCheckbox`: a number, 1 or 0. Shows `label` beside it.
|
|
98
|
+
* - `nativeChip`: with `value`, a toggle (1 or 0); without, an action that sends `activate`.
|
|
99
|
+
* - `nativeText`: shows `label`, or its bound value. Sends nothing.
|
|
100
|
+
*
|
|
101
|
+
* `label` is every control's accessible name. There are no tooltips.
|
|
102
|
+
*/
|
|
103
|
+
export type PanelSceneControlKind = "button" | "slider" | "knob" | "nativeButton" | "nativeSlider" | "nativeTextField" | "nativeDropdown" | "nativeSwitch" | "nativeCheckbox" | "nativeRadio" | "nativeSegmented" | "nativeChip" | "nativeText";
|
|
104
|
+
export interface PanelSceneControl {
|
|
79
105
|
id: string;
|
|
80
106
|
label: string;
|
|
81
|
-
kind:
|
|
107
|
+
kind: PanelSceneControlKind;
|
|
82
108
|
/** A button can open a native dropdown. Picking an item sends its event as the action. */
|
|
83
109
|
menu?: {
|
|
84
110
|
label: string;
|
|
85
111
|
event: string;
|
|
86
112
|
}[];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The control's rect over the artwork, in scene units. Required for the drawn kinds.
|
|
115
|
+
* A native control omits all four to be placed by `layout` instead.
|
|
116
|
+
*/
|
|
117
|
+
x?: number;
|
|
118
|
+
y?: number;
|
|
119
|
+
width?: number;
|
|
120
|
+
height?: number;
|
|
121
|
+
/**
|
|
122
|
+
* With a rect: the id of the layer this control paints directly after, so later layers draw
|
|
123
|
+
* over it. Omitted, the control paints above every layer.
|
|
124
|
+
*/
|
|
125
|
+
after?: string;
|
|
91
126
|
/** Omit event for local-only interaction (for example a chart inspection cursor). */
|
|
92
127
|
event?: string;
|
|
93
|
-
/**
|
|
128
|
+
/** `slider`, `knob`, `nativeSlider` and `nativeTextField` only: send coalesced change events while dragging or typing, plus the final commit. */
|
|
94
129
|
continuous?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* The name of the value this control shows and sets: in `values`, or in `strings` for a string kind.
|
|
132
|
+
* A choice's string must be `""` or one of its `options` (each part, for `multi`). `nativeButton` takes none.
|
|
133
|
+
*/
|
|
95
134
|
value?: string;
|
|
135
|
+
/** Choices for `nativeDropdown`, `nativeRadio` and `nativeSegmented`. Values are unique. */
|
|
136
|
+
options?: {
|
|
137
|
+
label: string;
|
|
138
|
+
value: string;
|
|
139
|
+
}[];
|
|
140
|
+
/** `nativeSegmented` only: choose several. */
|
|
141
|
+
multi?: boolean;
|
|
142
|
+
/** Shown but inert: no clicks, drags, keys or screen-reader adjustments act on it. */
|
|
143
|
+
disabled?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* 0 to 1, fixed or bound like a layer's. At 0 the control is gone: it takes no clicks,
|
|
146
|
+
* keyboard focus or screen-reader node. Anything between is only drawn fainter.
|
|
147
|
+
*/
|
|
148
|
+
opacity?: number | SceneBinding;
|
|
149
|
+
/** `nativeButton` only. Default `filled`. */
|
|
150
|
+
variant?: "filled" | "tonal" | "outlined" | "text";
|
|
151
|
+
/** `nativeButton` and `nativeChip`: a Material Design Icons name, such as `radio-tower`. */
|
|
152
|
+
icon?: string;
|
|
153
|
+
/** `nativeTextField` and `nativeDropdown`: shown while empty. */
|
|
154
|
+
placeholder?: string;
|
|
155
|
+
/** `nativeText` only: a typography role from the render environment. Default `body`. */
|
|
156
|
+
style?: "body" | "label" | "title" | "display" | "mono";
|
|
157
|
+
/** `nativeText` only. Default `start`. */
|
|
158
|
+
align?: "start" | "center" | "end";
|
|
96
159
|
min?: number;
|
|
97
160
|
max?: number;
|
|
98
161
|
step?: number;
|
|
@@ -110,12 +173,66 @@ export interface PanelSceneEvent {
|
|
|
110
173
|
action: string;
|
|
111
174
|
phase: "activate" | "change" | "commit";
|
|
112
175
|
sequence: number;
|
|
176
|
+
/** The control's number, for a control bound to `values`. */
|
|
113
177
|
value?: number;
|
|
178
|
+
/** The control's string, for a control bound to `strings`. */
|
|
179
|
+
text?: string;
|
|
114
180
|
}
|
|
115
|
-
/** Patch only named scene values. The host ignores stale responses. */
|
|
181
|
+
/** Patch only named scene values and strings. The host ignores stale responses. */
|
|
116
182
|
export interface PanelSceneEventResult {
|
|
117
183
|
values: Record<string, number>;
|
|
184
|
+
strings?: Record<string, string>;
|
|
118
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Flutter's Row, Column and Wrap, with Flexible, Spacer and SizedBox as fields.
|
|
188
|
+
* Sizes are logical pixels, like the render environment's. At most 8 deep and 128 nodes.
|
|
189
|
+
*
|
|
190
|
+
* - `{row: [...]}` / `{column: [...]}` / `{wrap: [...]}` lay out their children.
|
|
191
|
+
* - `{control: "id"}` places a native control that has no rect. Each appears exactly once.
|
|
192
|
+
* - `{scene: true}` places the artwork, scaled to fit. At most once.
|
|
193
|
+
* - `{spacer: flex}` takes a share of the free space.
|
|
194
|
+
*
|
|
195
|
+
* A text field or dropdown given no width (a row child without `flex` or `width`) is 200 wide.
|
|
196
|
+
*/
|
|
197
|
+
export type PanelSceneLayoutNode = ({
|
|
198
|
+
row: PanelSceneLayoutNode[];
|
|
199
|
+
} | {
|
|
200
|
+
column: PanelSceneLayoutNode[];
|
|
201
|
+
} | {
|
|
202
|
+
wrap: PanelSceneLayoutNode[];
|
|
203
|
+
} | {
|
|
204
|
+
control: string;
|
|
205
|
+
} | {
|
|
206
|
+
scene: true;
|
|
207
|
+
} | {
|
|
208
|
+
spacer: number;
|
|
209
|
+
}) & {
|
|
210
|
+
/** Only on a row or column's direct child: share the free space, like Expanded (`tight`, the default) or Flexible (`loose`). */
|
|
211
|
+
flex?: number;
|
|
212
|
+
fit?: "tight" | "loose";
|
|
213
|
+
width?: number;
|
|
214
|
+
height?: number;
|
|
215
|
+
/** All sides, or `[start, top, end, bottom]`. */
|
|
216
|
+
padding?: number | [
|
|
217
|
+
number,
|
|
218
|
+
number,
|
|
219
|
+
number,
|
|
220
|
+
number
|
|
221
|
+
];
|
|
222
|
+
/** Gap between children; for a wrap, between items in a run. */
|
|
223
|
+
spacing?: number;
|
|
224
|
+
/** Wrap only: gap between runs. */
|
|
225
|
+
runSpacing?: number;
|
|
226
|
+
mainAxisAlignment?: "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly";
|
|
227
|
+
/** `stretch` on a row or column only. */
|
|
228
|
+
crossAxisAlignment?: "start" | "center" | "end" | "stretch";
|
|
229
|
+
};
|
|
230
|
+
/** @deprecated Renamed to `PanelScene`. */
|
|
231
|
+
export type SvgScene = PanelScene;
|
|
232
|
+
/** @deprecated Renamed to `PanelSceneLayer`. */
|
|
233
|
+
export type SvgSceneLayer = PanelSceneLayer;
|
|
234
|
+
/** @deprecated Renamed to `PanelSceneControl`. */
|
|
235
|
+
export type SvgSceneControl = PanelSceneControl;
|
|
119
236
|
export declare const HOOK_CATEGORIES: readonly [
|
|
120
237
|
"account",
|
|
121
238
|
"activity",
|
|
@@ -413,6 +530,7 @@ export interface RefGeojson {
|
|
|
413
530
|
url: string;
|
|
414
531
|
maxAgeInDays?: number;
|
|
415
532
|
missMaxAgeInDays?: number;
|
|
533
|
+
sourceName?: string;
|
|
416
534
|
}
|
|
417
535
|
export interface LoggingControlDescriptor {
|
|
418
536
|
key: string;
|
|
@@ -507,6 +625,12 @@ export interface ActivitySuggestion extends Ref {
|
|
|
507
625
|
relevance?: number;
|
|
508
626
|
allowsMultiple?: boolean;
|
|
509
627
|
}
|
|
628
|
+
export interface MapBounds {
|
|
629
|
+
south: number;
|
|
630
|
+
west: number;
|
|
631
|
+
north: number;
|
|
632
|
+
east: number;
|
|
633
|
+
}
|
|
510
634
|
export interface SuggestArgs {
|
|
511
635
|
operation: Record<string, JSONValue>;
|
|
512
636
|
location?: {
|
|
@@ -515,6 +639,7 @@ export interface SuggestArgs {
|
|
|
515
639
|
};
|
|
516
640
|
callsign?: string;
|
|
517
641
|
searchTerm?: string;
|
|
642
|
+
bounds?: MapBounds;
|
|
518
643
|
scoped?: boolean;
|
|
519
644
|
}
|
|
520
645
|
export interface ActivityHook {
|
|
@@ -818,6 +943,7 @@ export interface CommandAction {
|
|
|
818
943
|
qsoCount?: number;
|
|
819
944
|
};
|
|
820
945
|
devLogCat?: Record<string, never>;
|
|
946
|
+
devMapInfo?: Record<string, never>;
|
|
821
947
|
toggleExperiment?: {
|
|
822
948
|
token: string;
|
|
823
949
|
};
|
|
@@ -1140,6 +1266,20 @@ export interface FormField {
|
|
|
1140
1266
|
templateContext?: "export" | "adif" | "text";
|
|
1141
1267
|
templateSample?: Record<string, JSONValue>;
|
|
1142
1268
|
uppercase?: boolean;
|
|
1269
|
+
suggestions?: {
|
|
1270
|
+
code: string;
|
|
1271
|
+
name?: string;
|
|
1272
|
+
standalone?: boolean;
|
|
1273
|
+
}[];
|
|
1274
|
+
multiValue?: {
|
|
1275
|
+
separator: string;
|
|
1276
|
+
inheritPrefix?: number;
|
|
1277
|
+
};
|
|
1278
|
+
transforms?: {
|
|
1279
|
+
pattern: string;
|
|
1280
|
+
replacement: string;
|
|
1281
|
+
flags?: string;
|
|
1282
|
+
}[];
|
|
1143
1283
|
options?: FormFieldOption[] | string;
|
|
1144
1284
|
validate?: (value: any, state: Record<string, any>) => string | null | undefined | Promise<string | null | undefined>;
|
|
1145
1285
|
transform?: (value: any, state: Record<string, any>) => any | Promise<any>;
|
|
@@ -1397,7 +1537,7 @@ export interface PanelRadioTuneResult {
|
|
|
1397
1537
|
state: PanelRadioState | null;
|
|
1398
1538
|
}
|
|
1399
1539
|
export interface PanelRenderArgs {
|
|
1400
|
-
/** Present on environment-capable hosts. Changes automatically request a throttled render of
|
|
1540
|
+
/** Present on environment-capable hosts. Changes automatically request a throttled render of a `scene` panel. */
|
|
1401
1541
|
environment?: PanelEnvironment;
|
|
1402
1542
|
/** Display time follows developer time travel; real time is for network retry/cache budgets. */
|
|
1403
1543
|
clock?: {
|
|
@@ -1414,8 +1554,18 @@ export interface PanelRenderArgs {
|
|
|
1414
1554
|
reason: string;
|
|
1415
1555
|
}
|
|
1416
1556
|
export type PanelContent = {
|
|
1557
|
+
kind: "scene";
|
|
1558
|
+
scene: PanelScene;
|
|
1559
|
+
title?: string;
|
|
1560
|
+
triggers?: string[];
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* @deprecated The same kind as `scene`, under its name through SDK 0.9. Apps before extension
|
|
1564
|
+
* API 5 know only `svgScene`; `scene` needs `"api": 5`, as do native controls, `strings` and `layout`.
|
|
1565
|
+
*/
|
|
1566
|
+
| {
|
|
1417
1567
|
kind: "svgScene";
|
|
1418
|
-
scene:
|
|
1568
|
+
scene: PanelScene;
|
|
1419
1569
|
title?: string;
|
|
1420
1570
|
triggers?: string[];
|
|
1421
1571
|
} | {
|
|
@@ -1719,6 +1869,21 @@ export interface ReferenceActivityHooks {
|
|
|
1719
1869
|
adifImportHook: AdifImportHook;
|
|
1720
1870
|
}
|
|
1721
1871
|
export declare function referenceActivity(config: ReferenceActivityConfig): ReferenceActivityHooks;
|
|
1872
|
+
export interface NearbyLookupHost {
|
|
1873
|
+
dbLookupSelectByLocation(category: string, lat: number, lon: number, delta: number, activeOnly?: boolean): Promise<LookupRow[]>;
|
|
1874
|
+
dbLookupSelectInBounds(category: string, bounds: MapBounds, options?: {
|
|
1875
|
+
activeOnly?: boolean;
|
|
1876
|
+
limit?: number;
|
|
1877
|
+
}): Promise<LookupRow[]>;
|
|
1878
|
+
}
|
|
1879
|
+
export declare const VIEWPORT_LIMIT = 500;
|
|
1880
|
+
export declare function nearbyLookupRows(host: NearbyLookupHost, category: string, { location, bounds }: Pick<SuggestArgs, "location" | "bounds">, { delta, activeOnly }: {
|
|
1881
|
+
delta: number;
|
|
1882
|
+
activeOnly?: boolean;
|
|
1883
|
+
}): Promise<{
|
|
1884
|
+
rows: LookupRow[];
|
|
1885
|
+
inViewport: boolean;
|
|
1886
|
+
}>;
|
|
1722
1887
|
export declare class TemplateError extends Error {
|
|
1723
1888
|
readonly cause?: unknown;
|
|
1724
1889
|
constructor(message: string, options?: {
|
|
@@ -1822,6 +1987,10 @@ export declare const host: {
|
|
|
1822
1987
|
dbLookupSelectOne(category: string, key: string): Promise<LookupRow | null>;
|
|
1823
1988
|
dbLookupSelectAll(category: string, query: string, subCategory?: string, activeOnly?: boolean): Promise<LookupRow[]>;
|
|
1824
1989
|
dbLookupSelectByLocation(category: string, lat: number, lon: number, delta: number, activeOnly?: boolean): Promise<LookupRow[]>;
|
|
1990
|
+
dbLookupSelectInBounds(category: string, bounds: MapBounds, options?: {
|
|
1991
|
+
activeOnly?: boolean;
|
|
1992
|
+
limit?: number;
|
|
1993
|
+
}): Promise<LookupRow[]>;
|
|
1825
1994
|
showForm(form: FormDefinition, state?: Record<string, any>): Promise<Record<string, any> | null>;
|
|
1826
1995
|
statusBar: {
|
|
1827
1996
|
setMessage(params: StatusBarMessageParams): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { base64ToBytes, bytesToBase64 } from "./base64.js";
|
|
|
2
2
|
import { adoptExtensionTimers, clearInterval, clearTimeout, setInterval, setTimeout } from "./timers.js";
|
|
3
3
|
export * from "./types.js";
|
|
4
4
|
export * from "./hookCategories.js";
|
|
5
|
-
export * from "./
|
|
5
|
+
export * from "./panelScene.js";
|
|
6
6
|
export * from "./i18n.js";
|
|
7
7
|
export * from "./dxcc.js";
|
|
8
8
|
export * from "./location.js";
|
|
@@ -16,6 +16,7 @@ export * from "./activityAdifImport.js";
|
|
|
16
16
|
export * from "./exportNames.js";
|
|
17
17
|
export * from "./exportSettings.js";
|
|
18
18
|
export * from "./referenceActivity.js";
|
|
19
|
+
export * from "./nearby.js";
|
|
19
20
|
export * from "./refTransforms.js";
|
|
20
21
|
export * from "./templates.js";
|
|
21
22
|
export * from "./templateContext.js";
|
|
@@ -304,6 +305,13 @@ const host = {
|
|
|
304
305
|
async dbLookupSelectByLocation(category, lat, lon, delta, activeOnly) {
|
|
305
306
|
return await hostCallFn()("dbLookupSelectByLocation", { category, lat, lon, delta, activeOnly });
|
|
306
307
|
},
|
|
308
|
+
/// Every row inside `bounds`, nearest its centre first, at most `limit`
|
|
309
|
+
/// (default 500, never past 1000). `west` past `east` is a box across the
|
|
310
|
+
/// antimeridian. Only an app that sends `SuggestArgs.bounds` answers this
|
|
311
|
+
/// call — so call it only when `bounds` arrived, as `nearbyLookupRows` does.
|
|
312
|
+
async dbLookupSelectInBounds(category, bounds, options = {}) {
|
|
313
|
+
return await hostCallFn()("dbLookupSelectInBounds", { category, ...bounds, ...options });
|
|
314
|
+
},
|
|
307
315
|
async showForm(form, state = {}) {
|
|
308
316
|
return kernel().host.showForm(form, state, currentExtensionKey);
|
|
309
317
|
},
|
package/dist/nearby.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const VIEWPORT_LIMIT = 500;
|
|
2
|
+
async function nearbyLookupRows(host, category, { location, bounds }, { delta, activeOnly }) {
|
|
3
|
+
if (bounds) return { rows: await host.dbLookupSelectInBounds(category, bounds, { activeOnly, limit: VIEWPORT_LIMIT }), inViewport: true };
|
|
4
|
+
if (location) return { rows: await host.dbLookupSelectByLocation(category, location.lat, location.lon, delta, activeOnly), inViewport: false };
|
|
5
|
+
return { rows: [], inViewport: false };
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
VIEWPORT_LIMIT,
|
|
9
|
+
nearbyLookupRows
|
|
10
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nearbyLookupRows } from "./nearby.js";
|
|
1
2
|
import { distanceOnEarth } from "@ham2k/lib-geo-tools";
|
|
2
3
|
import { activityAdifImport } from "./activityAdifImport.js";
|
|
3
4
|
async function hostApi() {
|
|
@@ -186,9 +187,9 @@ function referenceActivity(config) {
|
|
|
186
187
|
];
|
|
187
188
|
}
|
|
188
189
|
} : {},
|
|
189
|
-
async suggest({ location, searchTerm, scoped }, ctx) {
|
|
190
|
+
async suggest({ location, bounds, searchTerm, scoped }, ctx) {
|
|
190
191
|
const host = await hostApi();
|
|
191
|
-
const rows = searchTerm ? await host.dbLookupSelectAll(category, searchTerm) :
|
|
192
|
+
const { rows, inViewport } = searchTerm ? { rows: await host.dbLookupSelectAll(category, searchTerm), inViewport: false } : await nearbyLookupRows(host, category, { location, bounds }, { delta: NEARBY_DELTA });
|
|
192
193
|
const withDistance = rows.map((row) => ({
|
|
193
194
|
row,
|
|
194
195
|
distance: location && row.lat != null && row.lon != null ? distanceOnEarth(location, { lat: row.lat, lon: row.lon }) ?? void 0 : void 0
|
|
@@ -196,7 +197,7 @@ function referenceActivity(config) {
|
|
|
196
197
|
if (!searchTerm) {
|
|
197
198
|
withDistance.sort((a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity));
|
|
198
199
|
}
|
|
199
|
-
const suggestions = withDistance.slice(0, MAX_SUGGESTIONS).map(({ row, distance }) => {
|
|
200
|
+
const suggestions = withDistance.slice(0, inViewport ? withDistance.length : MAX_SUGGESTIONS).map(({ row, distance }) => {
|
|
200
201
|
const data = row.data ?? {};
|
|
201
202
|
return {
|
|
202
203
|
type: config.activationType,
|
package/docs/distribution.md
CHANGED
|
@@ -51,7 +51,9 @@ reaches every app that can run it: 2 adds `host.webSocket`, and the packer
|
|
|
51
51
|
refuses a manifest declaring `webSockets` under anything less; 3 adds timers
|
|
52
52
|
and `onShow`/`onHide`, and from 3 the build rewrites a bare `setTimeout` into
|
|
53
53
|
the SDK's (README's "Timers"); 4 lets `host.fetch` reach a plain-`http://` host
|
|
54
|
-
the app fronts with its https proxy (README's `host.fetch`)
|
|
54
|
+
the app fronts with its https proxy (README's `host.fetch`); 5 adds panel
|
|
55
|
+
scenes' `scene` kind, native controls, `strings`, `layout`, `after` and control
|
|
56
|
+
`opacity` (hooks.md's `panel`).
|
|
55
57
|
|
|
56
58
|
Three fields are **refused** in a distributed bundle, and it is worth
|
|
57
59
|
understanding why:
|
|
@@ -260,7 +262,7 @@ It reports every problem at once rather than one per run:
|
|
|
260
262
|
h2kext-pack: ./build is not ready to package:
|
|
261
263
|
manifest.json: missing required field 'name'
|
|
262
264
|
manifest.json: key 'my-clock' must start with your callsign — 'my' is not one (e.g. ki2d-my-clock)
|
|
263
|
-
manifest.json: missing 'api' — declare the extension API this was built against (currently
|
|
265
|
+
manifest.json: missing 'api' — declare the extension API this was built against (currently 5)
|
|
264
266
|
```
|
|
265
267
|
|
|
266
268
|
`--force-name` waives the naming convention below. It exists for Ham2K's own
|
|
@@ -399,8 +401,9 @@ built-in's own state stays where it is.
|
|
|
399
401
|
One rule bends for these, and only for them:
|
|
400
402
|
|
|
401
403
|
- **Built-in collisions.** An installed bundle may not claim a key the app
|
|
402
|
-
ships — judged against what the app is currently OFFERING, which
|
|
403
|
-
catalog
|
|
404
|
+
ships — judged against what the app is currently OFFERING, which once the
|
|
405
|
+
upgrade to catalog extensions is taken is the core extensions and nothing
|
|
406
|
+
else. `ham2k-lookup`
|
|
404
407
|
is both a built-in and a pre-load, and could otherwise never install.
|
|
405
408
|
|
|
406
409
|
**Build secrets are not among them, and used to be.** A pre-load holding a
|
|
@@ -412,14 +415,16 @@ sees no build secret, whatever key it claims and wherever it came from.
|
|
|
412
415
|
|
|
413
416
|
### The upgrade to catalog extensions
|
|
414
417
|
|
|
415
|
-
|
|
416
|
-
at
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
418
|
+
A new install — one whose default profile has never had a callsign — starts
|
|
419
|
+
on the catalog at its first boot, on the bundles the app ships, with nothing
|
|
420
|
+
to offer and nothing to carry; the decision is recorded once
|
|
421
|
+
(`seedExtensionsMigratedDefault`). Onboarding still names its activities by
|
|
422
|
+
their built-in keys, which `offeredKeyFor` maps to the shipped twin that
|
|
423
|
+
actually runs. An install that has been running the app's own extensions is
|
|
424
|
+
offered the move at start, on every launch until it is taken. Only a taken upgrade withdraws
|
|
425
|
+
anything, and the offer may be deferred. A catalog that cannot be reached
|
|
426
|
+
raises no offer and says nothing: it is a launch nobody was watching, and the
|
|
427
|
+
offer comes back next time.
|
|
423
428
|
|
|
424
429
|
Accepting installs the pre-loads, then fetches the catalog's twin of every
|
|
425
430
|
other built-in the operator had switched on — `wca` becomes `ham2k-wca` —
|
package/docs/forms.md
CHANGED
|
@@ -38,6 +38,23 @@ export interface FormField {
|
|
|
38
38
|
value?: any; // initial state value if not provided in host call state
|
|
39
39
|
placeholder?: string;
|
|
40
40
|
uppercase?: boolean; // force typed input to uppercase (callsign fields always are)
|
|
41
|
+
// Only used when fieldType === 'text': the codes the field takes, offered
|
|
42
|
+
// in a list under it as the operator types (matched on code and name, as a
|
|
43
|
+
// `kind: 'options'` logging control matches) and named beside it — a value
|
|
44
|
+
// that is none of them shows in red with a `?` once the operator pauses or
|
|
45
|
+
// leaves the field. Flagged, never refused: Save writes what was typed.
|
|
46
|
+
// `standalone` marks a code valid only as the WHOLE value, never inside a
|
|
47
|
+
// multiValue list. A host older than this renders the plain text field, and
|
|
48
|
+
// so does a settings list (or a field with an `icon`), which edits text in a
|
|
49
|
+
// dialog row of its own.
|
|
50
|
+
suggestions?: { code: string; name?: string; standalone?: boolean }[];
|
|
51
|
+
// Only used with `suggestions`: the field holds several codes joined by
|
|
52
|
+
// `separator` (a QSO party county line, `ALB/SCH`), each suggested, named
|
|
53
|
+
// and checked on its own. `inheritPrefix` as on a logging control's.
|
|
54
|
+
multiValue?: { separator: string; inheritPrefix?: number };
|
|
55
|
+
// Only used with `suggestions`: live-reformatting passes as a logging
|
|
56
|
+
// control's `transforms` — a county line typed `ALB,SCH` becomes `ALB/SCH`.
|
|
57
|
+
transforms?: { pattern: string; replacement: string; flags?: string }[];
|
|
41
58
|
options?: FormFieldOption[]; // required for select/radio
|
|
42
59
|
validate?: (value: any, state: Record<string, any>) => string | null | undefined | Promise<string | null | undefined>;
|
|
43
60
|
transform?: (value: any, state: Record<string, any>) => any | Promise<any>;
|
package/docs/hooks.md
CHANGED
|
@@ -46,8 +46,8 @@ hook method receives `(args, ctx)` where `ctx: HookContext` is:
|
|
|
46
46
|
effect must stay invisible until an experiment gating the surface it
|
|
47
47
|
belongs to has shipped ungated — checked per-call, since gating at the
|
|
48
48
|
whole-extension `manifest.experiments` grain would also hide any other
|
|
49
|
-
hook the same extension registers (e.g. `radio-commands`'
|
|
50
|
-
checks `
|
|
49
|
+
hook the same extension registers (e.g. `radio-commands`' WPM command
|
|
50
|
+
checks `keyer` here; its BAND/MODE/frequency siblings don't).
|
|
51
51
|
* `experiments?: { key, name, aliases }[]` — The experiment catalog itself,
|
|
52
52
|
on or off, so a hook can resolve a typed key-or-alias to one of the keys
|
|
53
53
|
`enabledExperiments` lists (the EXP preview says "Enable"/"Disable" rather
|
|
@@ -473,15 +473,18 @@ its own program name reads the wrong program aloud. Programs built on the SDK's
|
|
|
473
473
|
from one `linkUrl` config line.
|
|
474
474
|
|
|
475
475
|
`geojsonUrlForRef` says where the reference's own footprint can be fetched as
|
|
476
|
-
GeoJSON — a SOTA summit's activation zone,
|
|
477
|
-
program publishes.
|
|
476
|
+
GeoJSON — a SOTA summit's activation zone, a POTA park's boundary, and
|
|
477
|
+
whatever equivalent another program publishes. Areas (`Polygon`,
|
|
478
|
+
`MultiPolygon`) are drawn filled with an edge; lines (`LineString`,
|
|
479
|
+
`MultiLineString`) — a trail or a river that is the park — are drawn as an
|
|
480
|
+
edge alone; points are ignored. Same contract as `linkForRef`: build the URL from the
|
|
478
481
|
reference, **never hit the network**, return `null` for a reference the program
|
|
479
482
|
has no shape for. The core does the fetching and the caching (`GeoshapeService`),
|
|
480
483
|
so an extension never decides how long a device keeps a file or how large one may
|
|
481
484
|
be; it only names the two ages the core should use:
|
|
482
485
|
|
|
483
486
|
```ts
|
|
484
|
-
// RefGeojson: {url, maxAgeInDays?, missMaxAgeInDays?}
|
|
487
|
+
// RefGeojson: {url, maxAgeInDays?, missMaxAgeInDays?, sourceName?}
|
|
485
488
|
```
|
|
486
489
|
|
|
487
490
|
`maxAgeInDays` (default 30) is how long a fetched shape stays usable offline —
|
|
@@ -490,6 +493,14 @@ long the core remembers that a reference has NO shape, deliberately shorter so a
|
|
|
490
493
|
newly surveyed zone appears within a day rather than a month. Both are ignored
|
|
491
494
|
unless positive: a zero would re-fetch on every rebuild of the map.
|
|
492
495
|
|
|
496
|
+
`sourceName` is who drew the shapes, short enough for the credit line a map
|
|
497
|
+
shows under its outlines ("Source: OSM, sotl.as") — the host of `url` when it
|
|
498
|
+
is left out. Name the data's owner, not the server that relays it: POTA's
|
|
499
|
+
outlines are served by Ham2K and credited to OSM.
|
|
500
|
+
|
|
501
|
+
A document past 16MB is not kept: the core remembers it as a miss, on
|
|
502
|
+
`missMaxAgeInDays`, rather than downloading it again on every retry.
|
|
503
|
+
|
|
493
504
|
The URL must be `https`, and the core does not follow redirects to reach it.
|
|
494
505
|
Unlike a link, this one is fetched rather than handed to the platform, and a
|
|
495
506
|
plaintext one — named outright, or arrived at through a `Location:` header —
|
|
@@ -497,10 +508,13 @@ would downgrade every activator's connection on a program's say-so.
|
|
|
497
508
|
|
|
498
509
|
Shapes are asked for sparingly: the Map tab asks about the operation's own
|
|
499
510
|
references, and the Activities pane's results map about those plus the one
|
|
500
|
-
suggestion whose card is open
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
511
|
+
suggestion whose card is open, drawn solid. The rest of the map's suggestions
|
|
512
|
+
are asked about only once it is zoomed in (from GL zoom 7, fading in by 8)
|
|
513
|
+
and drawn as a dashed edge under the solid ones — a search answers with dozens
|
|
514
|
+
of references, and further out a footprint is a few pixels across. Implemented by: `sota` (both types, via
|
|
515
|
+
SOTLAS's `az.sotl.as`, used with the operators' permission) and `pota` (both
|
|
516
|
+
types, via `services.ham2k.net/lookups/outlines/pota/`, which links parks to
|
|
517
|
+
OpenStreetMap shapes; a park not yet linked answers 404).
|
|
504
518
|
|
|
505
519
|
`suggestOperationTitle` lets an activity build a natural-language operation
|
|
506
520
|
title/subtitle from its refs — "at Yosemite NP" (POTA), "for Field Day"
|
|
@@ -537,7 +551,7 @@ interface ActivityHook {
|
|
|
537
551
|
// extension only says what the short form is.
|
|
538
552
|
// `skipFocus: true` takes a main logging field out of the Space and Tab cycle;
|
|
539
553
|
// the arrow keys still reach it. Every shipped contest sets it on Our Serial.
|
|
540
|
-
// SuggestArgs: {operation, location?: {lat, lon}, callsign?, searchTerm?, scoped
|
|
554
|
+
// SuggestArgs: {operation, location?: {lat, lon}, callsign?, searchTerm?, scoped?, bounds?: {south, west, north, east}}
|
|
541
555
|
// ActivitySuggestion: Ref & {distance?, relevance?, allowsMultiple?}
|
|
542
556
|
|
|
543
557
|
// The input catalog. A new kind is earned by a different WIDGET, never by
|
|
@@ -622,7 +636,17 @@ proposing a new `input.kind` is a core PR, deliberately (DESIGN.md §5's
|
|
|
622
636
|
|
|
623
637
|
`suggest` powers the Activities view's "add activity" search: called with a
|
|
624
638
|
`location`/`callsign` the core resolves from the operation if set, else the
|
|
625
|
-
station defaults (extensions never need to know which).
|
|
639
|
+
station defaults (extensions never need to know which). When the operator pans
|
|
640
|
+
the Activities results map it asks again with the map's view as `bounds` (and
|
|
641
|
+
its centre as `location`, no `searchTerm`, the box's scope if it has one):
|
|
642
|
+
answer with EVERY reference inside the view, not the nearest few, or the view's
|
|
643
|
+
corners stay empty however far the operator pans. `nearbyLookupRows` reads
|
|
644
|
+
either case, through the `dbLookupSelectInBounds` host call for a view — only
|
|
645
|
+
an app that sends `bounds` answers that call, so an extension that calls it
|
|
646
|
+
only then runs on older apps unchanged, and one that predates `bounds` answers
|
|
647
|
+
with what is nearest the view's centre. Measure `distance` from the `location`
|
|
648
|
+
given either way; the core re-measures a panned answer from the operation's own
|
|
649
|
+
place before any card shows it. Called with no
|
|
626
650
|
`searchTerm` for nearby suggestions, or with one for name/code search — an
|
|
627
651
|
extension may honor either, both, or neither (returning `[]`). `scoped` is
|
|
628
652
|
true when THIS extension alone was asked — the operator tapped its "Activity
|
|
@@ -1389,17 +1413,25 @@ interface PanelHook {
|
|
|
1389
1413
|
// PanelDescriptor: {key, title, description?, icon?, preview?, on?, form?}
|
|
1390
1414
|
// PanelRenderArgs: {panelKey, operation, qso?, qsoCount, config, reason}
|
|
1391
1415
|
// PanelContent: {kind: 'markdown' | 'svg' | 'html', content, title?, triggers?}
|
|
1416
|
+
// | {kind: 'scene', scene, title?, triggers?} (see Panel scene reference)
|
|
1392
1417
|
```
|
|
1393
1418
|
|
|
1394
1419
|
The host addresses a panel as `ext:<hookKey>:<key>`, and that id is stored
|
|
1395
1420
|
inside saved layouts — renaming a `key` drops the panel out of every
|
|
1396
1421
|
arrangement holding it.
|
|
1397
1422
|
|
|
1398
|
-
An experimental fourth kind,
|
|
1399
|
-
local controls, and host-run animations.
|
|
1400
|
-
title?, triggers?}`; it does not have a
|
|
1423
|
+
An experimental fourth kind, the **panel scene**, adds SVG and text layers, numeric
|
|
1424
|
+
bindings, local controls, native Material controls, a layout, and host-run animations.
|
|
1425
|
+
Its payload is `{kind: 'scene', scene, title?, triggers?}`; it does not have a
|
|
1426
|
+
`content` string. `kind: 'svgScene'` is the same kind under its name through SDK 0.9,
|
|
1427
|
+
which the SDK keeps as a deprecated alias along with the `SvgScene*` type names.
|
|
1428
|
+
**`scene` and everything it adds are extension API 5**: native controls,
|
|
1429
|
+
`strings`, `layout`, `after` and control `opacity`. A bundle using any of them
|
|
1430
|
+
declares `"api": 5` ([distribution.md](distribution.md)), and an app speaking
|
|
1431
|
+
less refuses it at install rather than showing the panel as an unknown kind.
|
|
1432
|
+
A plain scene that must reach those apps keeps `svgScene` and its lower API. Scene-capable hosts pass
|
|
1401
1433
|
`instanceId` on render/event calls. `onEvent` handles control events and returns
|
|
1402
|
-
|
|
1434
|
+
value patches (`values`, and `strings` for string controls); local-only controls and animation frames make no bridge
|
|
1403
1435
|
calls. Controls with `continuous: true` send `change` events during dragging and a
|
|
1404
1436
|
final `commit` on release; pending movements are coalesced to the newest value.
|
|
1405
1437
|
Numeric text supports `scale`, `truncate`, `modulo`, and `minIntegerDigits`
|
|
@@ -1410,8 +1442,8 @@ may hold numbers or preformatted strings; control `valueLabels` supplies accessi
|
|
|
1410
1442
|
value descriptions. Transform/opacity bindings may also supply numeric `samples`,
|
|
1411
1443
|
equally spaced over their `input` range, instead of the `output` ramp. The host
|
|
1412
1444
|
interpolates and clamps these locally; chart markers and callouts can follow
|
|
1413
|
-
individual readings without bridge calls. See [the design notes and open milestones](https://github.com/ham2k/halo/blob/main/docs/design/
|
|
1414
|
-
and the `k2hrc-
|
|
1445
|
+
individual readings without bridge calls. See [the design notes and open milestones](https://github.com/ham2k/halo/blob/main/docs/design/panel-scenes.md)
|
|
1446
|
+
and the `k2hrc-panel-scenes` sample. This API is experimental.
|
|
1415
1447
|
|
|
1416
1448
|
Scene-capable hosts also supply `args.environment` on **render and event** calls:
|
|
1417
1449
|
|
|
@@ -1432,7 +1464,7 @@ Scene-capable hosts also supply `args.environment` on **render and event** calls
|
|
|
1432
1464
|
describe the resolved presentation context. Font names refer to host fonts;
|
|
1433
1465
|
they are not URLs or permission to load remote fonts.
|
|
1434
1466
|
|
|
1435
|
-
For a panel showing
|
|
1467
|
+
For a panel showing a scene, environment changes automatically request a
|
|
1436
1468
|
render through the existing throttle, even with no triggers declared. Hidden
|
|
1437
1469
|
panels catch up on reveal; repeated resize updates coalesce; a scene laid out for
|
|
1438
1470
|
an old environment cannot overwrite a newer one. Markdown and HTML panels are
|
|
@@ -1450,6 +1482,202 @@ Button controls can supply `menu: [{label, event}]` to open a native dropdown.
|
|
|
1450
1482
|
Selecting an item emits its `event` as the action with the original control ID;
|
|
1451
1483
|
dismissing the menu emits nothing.
|
|
1452
1484
|
|
|
1485
|
+
**Native controls.** Besides the drawn `button`, `slider` and `knob` — invisible
|
|
1486
|
+
hit areas over the extension's own artwork — a scene can use Material widgets
|
|
1487
|
+
the app themes itself: `nativeButton`, `nativeSlider`, `nativeTextField`,
|
|
1488
|
+
`nativeDropdown`, `nativeSwitch`, `nativeCheckbox`, `nativeRadio`,
|
|
1489
|
+
`nativeSegmented`, `nativeChip` and `nativeText`. `PanelSceneControl` in the SDK
|
|
1490
|
+
lists what each kind takes. String-valued controls (text field, dropdown, radio,
|
|
1491
|
+
segmented) bind to the scene's `strings` map, which sits beside `values`; a key
|
|
1492
|
+
is in one or the other, never both; a choice's string is `""` or one of its
|
|
1493
|
+
`options` (each comma-separated part, for `multi`). Their events carry `text` instead of `value`,
|
|
1494
|
+
and an `onEvent` result may return `strings` as well as `values`. A text layer
|
|
1495
|
+
whose `value` names a string shows it as-is. A text field commits on Enter, or
|
|
1496
|
+
on leaving it after typing; a render that lands while the operator is typing
|
|
1497
|
+
does not overwrite the typing. Every control's `label` is its accessible name;
|
|
1498
|
+
the app has no tooltips. A text field or dropdown in a panel keeps keyboard
|
|
1499
|
+
focus; clicking any other native control hands focus back to the callsign
|
|
1500
|
+
field, as clicking a button anywhere else in the Operation view does.
|
|
1501
|
+
|
|
1502
|
+
**Layout.** A native control either has an `x`/`y`/`width`/`height` rect over the
|
|
1503
|
+
artwork, like the drawn kinds — it takes the rect's width and its own height, up
|
|
1504
|
+
to the rect's, so give a field a rect tall enough for it — or omits all four and is placed by
|
|
1505
|
+
`scene.layout` — a tree of Flutter's `row`, `column` and `wrap`, with `flex`/`fit`
|
|
1506
|
+
(Expanded and Flexible), `width`/`height`, `padding`, `spacing`, the two axis
|
|
1507
|
+
alignments, `{spacer: flex}`, `{control: id}`, and `{scene: true}` for the
|
|
1508
|
+
artwork, scaled to fit. Sizes are logical pixels, like the render environment's.
|
|
1509
|
+
Every rect-less control appears in the layout exactly once; a layout with no
|
|
1510
|
+
`scene` node shows no artwork, so it may declare no layers either. A text field
|
|
1511
|
+
or dropdown given no width (a row child without `flex` or `width`) is 200 wide,
|
|
1512
|
+
and a row's children that do not fit overflow as a Flutter `Row`'s do — use
|
|
1513
|
+
`flex` or `wrap`. A `flex` only shares space its row or column has: in a row
|
|
1514
|
+
that is itself an unflexed child of a row, there is none, and its children lay
|
|
1515
|
+
out at their own size. The layout is at most 8 deep and 128 nodes.
|
|
1516
|
+
|
|
1517
|
+
**Controls among the layers.** Layers paint in order, and a control with a rect
|
|
1518
|
+
paints above all of them unless its `after` names a layer: then it paints
|
|
1519
|
+
directly after that one, and later layers draw over it. Layers are
|
|
1520
|
+
click-through: art painted over a control does not stop it working. A layer with
|
|
1521
|
+
`blocksPointer: true` takes the clicks that land on it while it is visible,
|
|
1522
|
+
covering the controls under it (keyboard focus still reaches them; mark them
|
|
1523
|
+
`disabled` too to shut them off). Only a pending action disables action
|
|
1524
|
+
controls; a value being sent does not. Any control can take an `opacity`, fixed or bound like a layer's;
|
|
1525
|
+
at 0 it takes no clicks, focus or screen-reader node.
|
|
1526
|
+
|
|
1527
|
+
### Panel scene reference
|
|
1528
|
+
|
|
1529
|
+
The schema in one place; `panelScene.ts` in the SDK carries the same as types.
|
|
1530
|
+
Every rule below is checked when the scene arrives: a scene that breaks one is
|
|
1531
|
+
refused whole, and the pane shows the error naming what is wrong, rather than
|
|
1532
|
+
drawing part of it.
|
|
1533
|
+
|
|
1534
|
+
**The scene document**
|
|
1535
|
+
|
|
1536
|
+
| Field | |
|
|
1537
|
+
|---|---|
|
|
1538
|
+
| `version` | `1` |
|
|
1539
|
+
| `width`, `height` | The artwork's viewBox, in scene units. Size it to `environment.width`/`height` and a scene unit is a logical pixel. |
|
|
1540
|
+
| `values` | `{name: number}`: what bindings, numeric text and numeric controls read. At most 256. |
|
|
1541
|
+
| `strings` | `{name: string}`: what text fields and choices hold. At most 256, each up to 4096 characters. A name is in `values` or `strings`, never both. |
|
|
1542
|
+
| `layers` | Painted in order, each over the ones before: `svg` artwork or `text`. At most 128, and 1 MiB of artwork and text in all. |
|
|
1543
|
+
| `controls` | At most 64. Drawn or native; see below. |
|
|
1544
|
+
| `layout` | Optional. Where rect-less native controls and the artwork go; omitted, the artwork fills the pane. |
|
|
1545
|
+
|
|
1546
|
+
**Controls.** Every control has an `id` and a `label`, which is its accessible
|
|
1547
|
+
name. An `event` names the action sent when it is used; without one the control
|
|
1548
|
+
is local-only: its value changes on screen, survives renders, and never reaches
|
|
1549
|
+
the extension. `value` names what it shows and sets.
|
|
1550
|
+
|
|
1551
|
+
| Kind | Binds to | Sends | Also takes |
|
|
1552
|
+
|---|---|---|---|
|
|
1553
|
+
| `button` | — | `activate` | `menu` |
|
|
1554
|
+
| `slider` | number | `commit` | `min`, `max`, `step`, `discrete`, `hover`, `valueLabels`, `continuous` |
|
|
1555
|
+
| `knob` | number | `commit` | `min`, `max`, `step`, `sensitivity`, `valueLabels`, `continuous` |
|
|
1556
|
+
| `nativeButton` | — | `activate` | `variant` (`filled`, `tonal`, `outlined`, `text`), `icon`, `menu`; needs `event` or `menu` |
|
|
1557
|
+
| `nativeSlider` | number | `commit` | `min`, `max`, `step`, `valueLabels`, `continuous` |
|
|
1558
|
+
| `nativeTextField` | string | `commit` on Enter, or on leaving it after typing | `placeholder`, `continuous` |
|
|
1559
|
+
| `nativeDropdown` | string | `commit` | `options`, `placeholder` |
|
|
1560
|
+
| `nativeRadio` | string | `commit` | `options` |
|
|
1561
|
+
| `nativeSegmented` | string | `commit` | `options`, `multi` |
|
|
1562
|
+
| `nativeSwitch`, `nativeCheckbox` | number, 1 or 0 | `commit` | — (shows `label` beside it) |
|
|
1563
|
+
| `nativeChip` | number, 1 or 0; or none | `commit`; `activate` without a value | `icon`; needs `event` without a value |
|
|
1564
|
+
| `nativeText` | number, string, or none | nothing | `style` (a typography role), `align` (`start`, `center`, `end`) |
|
|
1565
|
+
|
|
1566
|
+
The three drawn kinds are invisible hit areas over artwork the extension paints;
|
|
1567
|
+
the native kinds are Material widgets the app paints and themes. Every control
|
|
1568
|
+
may also take:
|
|
1569
|
+
|
|
1570
|
+
- `x`, `y`, `width`, `height`: its rect over the artwork, in scene units.
|
|
1571
|
+
Required for the drawn kinds; a native control omits all four to be placed by
|
|
1572
|
+
`layout` instead, and takes the rect's width and its own height when it has one.
|
|
1573
|
+
- `after`: with a rect, the layer it paints directly after.
|
|
1574
|
+
- `opacity`: 0 to 1, fixed or bound like a layer's. At 0 the control is gone.
|
|
1575
|
+
- `disabled`: shown, but no click, drag, key or screen-reader adjustment acts on it.
|
|
1576
|
+
|
|
1577
|
+
`options` is `[{label, value}]`, up to 64, with each `value` unique. A choice's
|
|
1578
|
+
string is `""` or one of them; a `multi` choice holds its values joined with
|
|
1579
|
+
commas in option order, so no option may contain one. `menu` is
|
|
1580
|
+
`[{label, event}]`, up to 32: picking an item sends its `event` as the action.
|
|
1581
|
+
|
|
1582
|
+
**Layout nodes.** Each is an object with exactly one of these keys:
|
|
1583
|
+
|
|
1584
|
+
| Node | |
|
|
1585
|
+
|---|---|
|
|
1586
|
+
| `{row: [...]}`, `{column: [...]}` | Flutter's `Row` and `Column`. `mainAxisAlignment` (`start`, `center`, `end`, `spaceBetween`, `spaceAround`, `spaceEvenly`), `crossAxisAlignment` (`start`, `center`, `end`, `stretch`), `spacing`. |
|
|
1587
|
+
| `{wrap: [...]}` | Flutter's `Wrap`: `spacing`, `runSpacing`, the same alignments but no `stretch`. |
|
|
1588
|
+
| `{control: "id"}` | A native control without a rect. Each appears exactly once. |
|
|
1589
|
+
| `{scene: true}` | The artwork, scaled to fit. At most once. |
|
|
1590
|
+
| `{spacer: n}` | `Spacer(flex: n)`. |
|
|
1591
|
+
|
|
1592
|
+
Any node may add `width`, `height` and `padding` (one number, or
|
|
1593
|
+
`[start, top, end, bottom]`). A row or column's direct child may add `flex`, with
|
|
1594
|
+
`fit: "tight"` (the default, Flutter's `Expanded`) or `"loose"` (`Flexible`). At
|
|
1595
|
+
most 8 deep and 128 nodes.
|
|
1596
|
+
|
|
1597
|
+
**Events and results.** `onEvent` receives, beside the render args:
|
|
1598
|
+
|
|
1599
|
+
```ts
|
|
1600
|
+
event: {
|
|
1601
|
+
controlId: string // the control's id
|
|
1602
|
+
action: string // its `event`, or the menu item's
|
|
1603
|
+
phase: "activate" | "change" | "commit"
|
|
1604
|
+
sequence: number // increases with every event from this pane
|
|
1605
|
+
value?: number // for a control bound to `values`
|
|
1606
|
+
text?: string // for a control bound to `strings`
|
|
1607
|
+
}
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
It answers `{values, strings?}`: a patch of names already in the scene. An
|
|
1611
|
+
answer with neither is taken as a failure, and so is one that throws or runs out
|
|
1612
|
+
of time: the pane says the action failed, and the control goes back to what the
|
|
1613
|
+
scene last reported. The host sends one event at a time, keeps only the newest
|
|
1614
|
+
of a control's queued changes, ignores an answer to an event a newer one has
|
|
1615
|
+
superseded, and requests a render after each. Action controls are disabled while an
|
|
1616
|
+
action is waiting for its answer; a value being sent does not disable them. A text field's typing is sent before any action, and an
|
|
1617
|
+
action queued behind a value the extension refused is not sent.
|
|
1618
|
+
|
|
1619
|
+
**An example.** A spot form over a status strip:
|
|
1620
|
+
|
|
1621
|
+
```ts
|
|
1622
|
+
const state = { call: "", band: "20m", status: "Nothing spotted yet" }
|
|
1623
|
+
|
|
1624
|
+
export const spotter: PanelHook = {
|
|
1625
|
+
async getPanels() {
|
|
1626
|
+
return [{ key: "spotter", title: "Spotter", icon: "bullhorn" }]
|
|
1627
|
+
},
|
|
1628
|
+
async render() {
|
|
1629
|
+
return {
|
|
1630
|
+
kind: "scene",
|
|
1631
|
+
scene: {
|
|
1632
|
+
version: 1,
|
|
1633
|
+
width: 400,
|
|
1634
|
+
height: 40,
|
|
1635
|
+
values: {},
|
|
1636
|
+
strings: state,
|
|
1637
|
+
layers: [
|
|
1638
|
+
{
|
|
1639
|
+
id: "strip", x: 0, y: 0, width: 400, height: 40,
|
|
1640
|
+
svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 40"><rect width="400" height="40" rx="8" fill="#142337"/></svg>',
|
|
1641
|
+
},
|
|
1642
|
+
{ id: "status", x: 12, y: 8, width: 376, height: 24, text: { value: "status", size: 14, color: "#a9efcf" } },
|
|
1643
|
+
],
|
|
1644
|
+
controls: [
|
|
1645
|
+
{ id: "call", kind: "nativeTextField", label: "Callsign", value: "call", event: "set" },
|
|
1646
|
+
{
|
|
1647
|
+
id: "band", kind: "nativeDropdown", label: "Band", value: "band", event: "set",
|
|
1648
|
+
options: ["40m", "20m", "15m"].map((b) => ({ label: b, value: b })),
|
|
1649
|
+
},
|
|
1650
|
+
{ id: "spot", kind: "nativeButton", label: "Spot", icon: "bullhorn", event: "spot" },
|
|
1651
|
+
],
|
|
1652
|
+
layout: {
|
|
1653
|
+
column: [
|
|
1654
|
+
{ row: [{ control: "call", flex: 1 }, { control: "band", width: 110 }, { control: "spot" }], spacing: 8 },
|
|
1655
|
+
{ scene: true, flex: 1 },
|
|
1656
|
+
],
|
|
1657
|
+
padding: 12,
|
|
1658
|
+
spacing: 8,
|
|
1659
|
+
crossAxisAlignment: "stretch",
|
|
1660
|
+
},
|
|
1661
|
+
},
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
async onEvent({ event }) {
|
|
1665
|
+
if (event.controlId === "call" && event.text !== undefined) state.call = event.text.toUpperCase()
|
|
1666
|
+
if (event.controlId === "band" && event.text !== undefined) state.band = event.text
|
|
1667
|
+
if (event.action === "spot") {
|
|
1668
|
+
if (!state.call) throw new Error("No callsign to spot")
|
|
1669
|
+
state.status = `Spotted ${state.call} on ${state.band}`
|
|
1670
|
+
}
|
|
1671
|
+
return { values: {}, strings: state }
|
|
1672
|
+
},
|
|
1673
|
+
}
|
|
1674
|
+
```
|
|
1675
|
+
|
|
1676
|
+
The callsign comes back upper-cased, and the field shows the extension's
|
|
1677
|
+
version once the typing is committed. A bundle carrying this declares
|
|
1678
|
+
`"api": 5`. The `k2hrc-panel-scenes` sample in the SDK has every kind, a
|
|
1679
|
+
control faded in over the artwork, and a click-through layer painted over it.
|
|
1680
|
+
|
|
1453
1681
|
The `ki2d-weather-panel`, `ki2d-solar-panel` and `ki2d-radio-panel` reference
|
|
1454
1682
|
panels live in
|
|
1455
1683
|
[ham2k/extensions](https://github.com/ham2k/extensions/tree/main/extensions/dashboard).
|
package/package.json
CHANGED
package/samples/README.md
CHANGED
|
@@ -10,7 +10,7 @@ building — the header comment in each `src/index.ts` says what it leaves out.
|
|
|
10
10
|
| `k2hrc-llota` | An award program: references, an offline list, activation scoring |
|
|
11
11
|
| `k2hrc-cqww` | A contest: an exchange to type, and a score to keep |
|
|
12
12
|
| `k2hrc-radio` | An HTML panel, and why you should probably write markdown instead |
|
|
13
|
-
| `k2hrc-
|
|
13
|
+
| `k2hrc-panel-scenes` | Experimental panel scenes: SVG artwork, controls and animations, with simulated radio and weather panels |
|
|
14
14
|
|
|
15
15
|
## Running one
|
|
16
16
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Panel Scene Prototypes
|
|
2
2
|
|
|
3
|
-
Experimental radio front face
|
|
4
|
-
`
|
|
3
|
+
Experimental radio front face, weather inspection chart and native controls form, using the public
|
|
4
|
+
`scene` / `PanelHook.onEvent` contracts. Requires a host with panel scene support.
|
|
5
5
|
All data is simulated; this extension does not fetch weather or control a radio.
|
|
6
6
|
|
|
7
7
|
The radio keeps requested frequency separate from reported frequency, simulates
|
|
@@ -16,9 +16,9 @@ In this repository:
|
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
18
|
node build.mjs
|
|
19
|
-
node ../../tools/h2kext-pack.mjs build -o /tmp/k2hrc-
|
|
19
|
+
node ../../tools/h2kext-pack.mjs build -o /tmp/k2hrc-panel-scenes.h2kext
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
For the contract and migration plan, see `docs/design/
|
|
22
|
+
For the contract and migration plan, see `docs/design/panel-scenes.md` in the
|
|
23
23
|
logger repository. This is an API experiment,
|
|
24
24
|
not a replacement for the production weather/solar panels yet.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"key": "k2hrc-
|
|
3
|
-
"name": "
|
|
4
|
-
"shortName": "
|
|
2
|
+
"key": "k2hrc-panel-scenes",
|
|
3
|
+
"name": "Panel Scene Prototypes",
|
|
4
|
+
"shortName": "Panel Scenes",
|
|
5
5
|
"version": "0.1.0",
|
|
6
|
-
"description": "Experimental radio front face and
|
|
6
|
+
"description": "Experimental radio front face, weather chart and native controls; simulated data only",
|
|
7
7
|
"category": "dashboard",
|
|
8
8
|
"icon": "radio-tower",
|
|
9
9
|
"accentColor": "#4C6EF5",
|
|
10
|
-
"api":
|
|
10
|
+
"api": 5,
|
|
11
11
|
"keywords": ["svg", "panel", "sample", "weather", "radio"],
|
|
12
12
|
"hooks": ["panel"],
|
|
13
13
|
"sharedDependencies": {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// Copyright ©️ 2026 Sebastian Delmont <sd@ham2k.com>
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import type { PanelContent, PanelHook, SceneBinding,
|
|
3
|
+
import type { PanelContent, PanelHook, PanelScene, SceneBinding, PanelSceneLayer } from "@ham2k/extension-sdk"
|
|
4
4
|
|
|
5
5
|
const svg = (body: string, width = 640, height = 360) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${body}</svg>`
|
|
6
6
|
const bind = (value: string, input: [number, number], output: [number, number]): SceneBinding => ({ value, input, output })
|
|
7
7
|
const label = (x: number, y: number, text: string, size = 16, color = "#9aaec6") =>
|
|
8
8
|
`<text x="${x}" y="${y}" fill="${color}" font-family="sans-serif" font-size="${size}">${text}</text>`
|
|
9
9
|
const button = (x: number, text: string) => `<rect x="${x}" y="298" width="172" height="38" rx="8" fill="#283b52"/>${label(x + 12, 322, text, 14, "#e7edf4")}`
|
|
10
|
-
const textLayer = (id: string, value: string, x: number, y: number, width: number, size: number, prefix = "", suffix = "", decimals = 0):
|
|
10
|
+
const textLayer = (id: string, value: string, x: number, y: number, width: number, size: number, prefix = "", suffix = "", decimals = 0): PanelSceneLayer => ({
|
|
11
11
|
id,
|
|
12
12
|
x,
|
|
13
13
|
y,
|
|
@@ -17,9 +17,23 @@ const textLayer = (id: string, value: string, x: number, y: number, width: numbe
|
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
type RadioState = { requested: number; reported: number; connected: number; due: number; slow: number }
|
|
20
|
+
type FormState = { strings: Record<string, string>; values: Record<string, number> }
|
|
20
21
|
/** Inject time for deterministic tests; no timers or per-frame extension work. */
|
|
21
22
|
export function createScenePanels(now: () => number = () => Date.now()): PanelHook {
|
|
22
23
|
const radios = new Map<string, RadioState>()
|
|
24
|
+
const forms = new Map<string, FormState>()
|
|
25
|
+
function form(id: string): FormState {
|
|
26
|
+
let f = forms.get(id)
|
|
27
|
+
if (!f) {
|
|
28
|
+
if (forms.size >= 64) forms.delete(forms.keys().next().value!)
|
|
29
|
+
f = {
|
|
30
|
+
strings: { call: "", band: "20m", mode: "CW", antenna: "vertical", status: "Nothing spotted yet" },
|
|
31
|
+
values: { power: 100, qrp: 0, cluster: 1, cwOnly: 0, spotted: 0 },
|
|
32
|
+
}
|
|
33
|
+
forms.set(id, f)
|
|
34
|
+
}
|
|
35
|
+
return f
|
|
36
|
+
}
|
|
23
37
|
function state(id: string): RadioState {
|
|
24
38
|
let s = radios.get(id)
|
|
25
39
|
if (!s) {
|
|
@@ -39,14 +53,16 @@ export function createScenePanels(now: () => number = () => Date.now()): PanelHo
|
|
|
39
53
|
return [
|
|
40
54
|
{ key: "radio", title: "Radio Front Face · Simulation", icon: "radio-tower", multiple: true, on: ["tick:1"] },
|
|
41
55
|
{ key: "weather", title: "Weather Chart · Simulation", icon: "weather-partly-cloudy", multiple: true },
|
|
56
|
+
{ key: "controls", title: "Native Controls · Simulation", icon: "form-textbox", multiple: true },
|
|
42
57
|
]
|
|
43
58
|
},
|
|
44
59
|
async render(args): Promise<PanelContent> {
|
|
60
|
+
if (args.panelKey === "controls") return { kind: "scene", scene: controlsScene(form(args.instanceId ?? args.panelKey)) }
|
|
45
61
|
if (args.panelKey === "weather") {
|
|
46
62
|
const temperatures = [17, 18, 21, 24, 25, 23, 20, 18]
|
|
47
63
|
const points = temperatures.map((t, i) => `${60 + i * 74},${265 - (t - 15) * 14}`).join(" ")
|
|
48
64
|
return {
|
|
49
|
-
kind: "
|
|
65
|
+
kind: "scene",
|
|
50
66
|
scene: {
|
|
51
67
|
version: 1,
|
|
52
68
|
width: 640,
|
|
@@ -91,7 +107,7 @@ export function createScenePanels(now: () => number = () => Date.now()): PanelHo
|
|
|
91
107
|
}
|
|
92
108
|
const s = state(args.instanceId ?? args.panelKey)
|
|
93
109
|
return {
|
|
94
|
-
kind: "
|
|
110
|
+
kind: "scene",
|
|
95
111
|
scene: {
|
|
96
112
|
version: 1,
|
|
97
113
|
width: 640,
|
|
@@ -213,8 +229,25 @@ export function createScenePanels(now: () => number = () => Date.now()): PanelHo
|
|
|
213
229
|
}
|
|
214
230
|
},
|
|
215
231
|
async onEvent(args) {
|
|
216
|
-
const s = state(args.instanceId ?? args.panelKey)
|
|
217
232
|
const event = args.event
|
|
233
|
+
if (args.panelKey === "controls") {
|
|
234
|
+
const f = form(args.instanceId ?? args.panelKey)
|
|
235
|
+
if (event.text !== undefined) f.strings[event.controlId] = event.text
|
|
236
|
+
if (event.value !== undefined) f.values[event.controlId] = event.value
|
|
237
|
+
if (event.action === "spot") {
|
|
238
|
+
const call = f.strings.call.trim().toUpperCase()
|
|
239
|
+
if (!call) throw new Error("No callsign to spot")
|
|
240
|
+
f.strings.call = call
|
|
241
|
+
f.values.spotted = 1
|
|
242
|
+
f.strings.status = `Spotted ${call} on ${f.strings.band} ${f.strings.mode}, ${f.values.qrp ? 5 : f.values.power} W`
|
|
243
|
+
}
|
|
244
|
+
if (event.action === "clear") {
|
|
245
|
+
f.values.spotted = 0
|
|
246
|
+
f.strings.status = "Nothing spotted yet"
|
|
247
|
+
}
|
|
248
|
+
return { values: f.values, strings: f.strings }
|
|
249
|
+
}
|
|
250
|
+
const s = state(args.instanceId ?? args.panelKey)
|
|
218
251
|
if (event.action === "connect") {
|
|
219
252
|
s.connected = 1 - s.connected
|
|
220
253
|
s.due = 0
|
|
@@ -241,3 +274,88 @@ export function createScenePanels(now: () => number = () => Date.now()): PanelHo
|
|
|
241
274
|
},
|
|
242
275
|
}
|
|
243
276
|
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Every native control kind, placed by a layout around a strip of artwork. Clear sits
|
|
280
|
+
* over the strip instead: ordered among its layers, under a click-through sheen, and
|
|
281
|
+
* faded out until there is something to clear.
|
|
282
|
+
*/
|
|
283
|
+
function controlsScene(f: FormState): PanelScene {
|
|
284
|
+
return {
|
|
285
|
+
version: 1,
|
|
286
|
+
width: 640,
|
|
287
|
+
height: 60,
|
|
288
|
+
values: f.values,
|
|
289
|
+
strings: f.strings,
|
|
290
|
+
layers: [
|
|
291
|
+
{ id: "strip", x: 0, y: 0, width: 640, height: 60, svg: svg('<rect width="640" height="60" rx="10" fill="#142337"/>', 640, 60) },
|
|
292
|
+
{ id: "status", x: 16, y: 16, width: 490, height: 28, text: { value: "status", size: 16, color: "#a9efcf" } },
|
|
293
|
+
{
|
|
294
|
+
id: "sheen",
|
|
295
|
+
x: 0,
|
|
296
|
+
y: 0,
|
|
297
|
+
width: 640,
|
|
298
|
+
height: 30,
|
|
299
|
+
svg: svg('<rect width="640" height="30" rx="10" fill="#ffffff" fill-opacity="0.08"/>', 640, 30),
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
controls: [
|
|
303
|
+
{ id: "call", kind: "nativeTextField", label: "Callsign", placeholder: "W1AW", value: "call", event: "set" },
|
|
304
|
+
{
|
|
305
|
+
id: "band",
|
|
306
|
+
kind: "nativeDropdown",
|
|
307
|
+
label: "Band",
|
|
308
|
+
value: "band",
|
|
309
|
+
event: "set",
|
|
310
|
+
options: ["40m", "20m", "15m", "10m"].map((b) => ({ label: b, value: b })),
|
|
311
|
+
},
|
|
312
|
+
{ id: "mode", kind: "nativeSegmented", label: "Mode", value: "mode", event: "set", options: ["CW", "SSB", "FT8"].map((m) => ({ label: m, value: m })) },
|
|
313
|
+
{ id: "spot", kind: "nativeButton", label: "Spot", icon: "bullhorn", event: "spot" },
|
|
314
|
+
{
|
|
315
|
+
id: "clear",
|
|
316
|
+
kind: "nativeButton",
|
|
317
|
+
label: "Clear",
|
|
318
|
+
variant: "outlined",
|
|
319
|
+
event: "clear",
|
|
320
|
+
after: "status",
|
|
321
|
+
x: 520,
|
|
322
|
+
y: 10,
|
|
323
|
+
width: 110,
|
|
324
|
+
height: 40,
|
|
325
|
+
opacity: { value: "spotted", input: [0, 1], output: [0, 1] },
|
|
326
|
+
},
|
|
327
|
+
{ id: "power", kind: "nativeSlider", label: "Power, watts", value: "power", event: "set", min: 5, max: 100, step: 5, disabled: f.values.qrp === 1 },
|
|
328
|
+
{ id: "qrp", kind: "nativeSwitch", label: "QRP", value: "qrp", event: "set" },
|
|
329
|
+
{ id: "cluster", kind: "nativeCheckbox", label: "Send to cluster", value: "cluster", event: "set" },
|
|
330
|
+
{
|
|
331
|
+
id: "antenna",
|
|
332
|
+
kind: "nativeRadio",
|
|
333
|
+
label: "Antenna",
|
|
334
|
+
value: "antenna",
|
|
335
|
+
event: "set",
|
|
336
|
+
options: [
|
|
337
|
+
{ label: "Vertical", value: "vertical" },
|
|
338
|
+
{ label: "Dipole", value: "dipole" },
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
{ id: "cw", kind: "nativeChip", label: "CW only", value: "cwOnly", icon: "filter" },
|
|
342
|
+
{ id: "powerText", kind: "nativeText", label: "Power", value: "power", style: "mono", align: "end" },
|
|
343
|
+
],
|
|
344
|
+
layout: {
|
|
345
|
+
column: [
|
|
346
|
+
{ row: [{ control: "call", flex: 1 }, { control: "band", width: 110 }, { control: "mode" }], spacing: 8 },
|
|
347
|
+
{ scene: true, flex: 1 },
|
|
348
|
+
{
|
|
349
|
+
wrap: [{ control: "qrp" }, { control: "cluster" }, { control: "antenna" }, { control: "cw" }],
|
|
350
|
+
spacing: 12,
|
|
351
|
+
runSpacing: 4,
|
|
352
|
+
crossAxisAlignment: "center",
|
|
353
|
+
},
|
|
354
|
+
{ row: [{ control: "power", flex: 1 }, { control: "powerText", width: 48 }, { control: "spot" }], spacing: 8 },
|
|
355
|
+
],
|
|
356
|
+
padding: 12,
|
|
357
|
+
spacing: 8,
|
|
358
|
+
crossAxisAlignment: "stretch",
|
|
359
|
+
},
|
|
360
|
+
}
|
|
361
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|