@ham2k/extension-sdk 0.1.0 → 0.2.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 +139 -0
- package/README.md +19 -4
- package/dist/index.d.ts +1 -1
- package/docs/distribution.md +279 -0
- package/docs/forms.md +271 -0
- package/docs/hooks.md +1282 -0
- package/docs/settings.md +523 -0
- package/docs/templates.md +204 -0
- package/package.json +12 -2
- package/samples/README.md +33 -0
- package/samples/k2hrc-cqww/build.mjs +6 -0
- package/samples/k2hrc-cqww/manifest.json +24 -0
- package/samples/k2hrc-cqww/src/index.ts +273 -0
- package/samples/k2hrc-hamqth/build.mjs +6 -0
- package/samples/k2hrc-hamqth/manifest.json +25 -0
- package/samples/k2hrc-hamqth/src/index.ts +202 -0
- package/samples/k2hrc-llota/build.mjs +6 -0
- package/samples/k2hrc-llota/manifest.json +25 -0
- package/samples/k2hrc-llota/src/index.ts +172 -0
- package/samples/k2hrc-radio/build.mjs +6 -0
- package/samples/k2hrc-radio/manifest.json +24 -0
- package/samples/k2hrc-radio/src/index.ts +145 -0
package/docs/forms.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# Form description schema
|
|
2
|
+
|
|
3
|
+
Extensions can use the Form description schema to request user input through complex forms rendered in either a modal dialog or a dedicated view screen.
|
|
4
|
+
|
|
5
|
+
Forms are structured as a list of fields, headers, section dividers, and markdown text blocks. They also support initial state values and asynchronous validation/transformation callbacks executed back in the JavaScript runtime.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Form Definition Schema
|
|
10
|
+
|
|
11
|
+
All types are defined in `extensions/sdk/src/types.ts`.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
export type FormFieldType =
|
|
15
|
+
| 'text'
|
|
16
|
+
| 'multiline'
|
|
17
|
+
| 'email'
|
|
18
|
+
| 'callsign'
|
|
19
|
+
| 'number'
|
|
20
|
+
| 'select'
|
|
21
|
+
| 'radio'
|
|
22
|
+
| 'checkbox'
|
|
23
|
+
| 'secret' // obscured text, e.g. passwords/API keys
|
|
24
|
+
| 'list' // a repeatable list of items, each shaped like itemFields
|
|
25
|
+
| 'account'; // a read-only reference into the separate account hook system
|
|
26
|
+
|
|
27
|
+
export interface FormFieldOption {
|
|
28
|
+
label: string;
|
|
29
|
+
value: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FormField {
|
|
33
|
+
type: 'field';
|
|
34
|
+
fieldType: FormFieldType;
|
|
35
|
+
key: string;
|
|
36
|
+
label: string;
|
|
37
|
+
value?: any; // initial state value if not provided in host call state
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
uppercase?: boolean; // force typed input to uppercase (callsign fields always are)
|
|
40
|
+
options?: FormFieldOption[]; // required for select/radio
|
|
41
|
+
validate?: (value: any, state: Record<string, any>) => string | null | undefined | Promise<string | null | undefined>;
|
|
42
|
+
transform?: (value: any, state: Record<string, any>) => any | Promise<any>;
|
|
43
|
+
// Only used when fieldType === 'list'. `value` is then a
|
|
44
|
+
// Record<string, any>[]; each item is shaped like itemFields. By default
|
|
45
|
+
// the field renders as one summary row (from itemTitle, templated
|
|
46
|
+
// against the item's values, e.g. "${name}" — NOT "{{name}}") that opens
|
|
47
|
+
// an editor dialog with per-item rows and add/edit/delete; `inline: true`
|
|
48
|
+
// instead renders those rows (with reorder handle/delete per row, and an
|
|
49
|
+
// Add row) directly in the form, skipping that outer dialog — for a list
|
|
50
|
+
// that's the main content of its own section. Either way, tapping a row
|
|
51
|
+
// (or Add) opens the same itemFields editor dialog (the form renderer
|
|
52
|
+
// recursively, scoped to itemFields).
|
|
53
|
+
itemFields?: FormField[];
|
|
54
|
+
itemTitle?: string;
|
|
55
|
+
orderable?: boolean;
|
|
56
|
+
inline?: boolean;
|
|
57
|
+
// Singular noun for one item (e.g. "custom note file"), used in the
|
|
58
|
+
// editor dialog's Add/Edit titles and the Add button instead of `label`
|
|
59
|
+
// — which names the list as a whole and is typically plural. Defaults to
|
|
60
|
+
// `label` when omitted.
|
|
61
|
+
itemLabel?: string;
|
|
62
|
+
// Only used when fieldType === 'account'. See settings.md's "account
|
|
63
|
+
// field type" section — this never defines or stores account data
|
|
64
|
+
// itself, it's a pointer into the separate `account` hook system.
|
|
65
|
+
account?: { key: string; subkey?: string };
|
|
66
|
+
// Hidden entirely unless the app's developer mode is on; when shown,
|
|
67
|
+
// rendered in the orange "dev mode" accent (label/icon/subtitle tinted,
|
|
68
|
+
// no badge or border — matches app-polo's `styles.colors.devMode`
|
|
69
|
+
// convention). Every FormElement variant below supports this, not just
|
|
70
|
+
// fields — a whole section header can be dev-mode-only too.
|
|
71
|
+
devMode?: boolean;
|
|
72
|
+
// Settings-panel-only (ignored in ad hoc forms) — see settings.md's
|
|
73
|
+
// "Common Preferences and environment gating". Also shows this field on
|
|
74
|
+
// the app's Common Preferences quick-access panel.
|
|
75
|
+
common?: boolean;
|
|
76
|
+
// Settings-panel-only. Restricts which platform(s) show this field at
|
|
77
|
+
// all: one or more platform tokens (`ios`, `android`, `macos`,
|
|
78
|
+
// `windows`, `linux`, `web`), comma-separated or as an array. Unprefixed
|
|
79
|
+
// tokens are a whitelist; `-`-prefixed tokens are a blacklist. See
|
|
80
|
+
// settings.md.
|
|
81
|
+
environment?: string | string[];
|
|
82
|
+
// Settings-panel-only. Opts this field into a target-scoped settings
|
|
83
|
+
// modal elsewhere in the app — see settings.md's "Settings targets".
|
|
84
|
+
target?: string | string[];
|
|
85
|
+
// Only meaningful within a `kind: 'dynamic'` settingsPanel. After this
|
|
86
|
+
// field commits, the app restarts the extension runtime — for a value
|
|
87
|
+
// only consulted at onActivation (e.g. a list of URLs to register as
|
|
88
|
+
// dataFile hooks). See settings.md's Tier 2 section.
|
|
89
|
+
restartOnChange?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface FormHeader {
|
|
93
|
+
type: 'header';
|
|
94
|
+
title: string;
|
|
95
|
+
subtitle?: string;
|
|
96
|
+
devMode?: boolean;
|
|
97
|
+
environment?: string | string[]; // settings-panel-only; see FormField.environment (not `common` — see settings.md)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface FormSectionDivider {
|
|
101
|
+
type: 'divider';
|
|
102
|
+
title?: string;
|
|
103
|
+
devMode?: boolean;
|
|
104
|
+
environment?: string | string[]; // settings-panel-only; see FormField.environment
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface FormMarkdownBlock {
|
|
108
|
+
type: 'markdown';
|
|
109
|
+
text: string;
|
|
110
|
+
devMode?: boolean;
|
|
111
|
+
environment?: string | string[]; // settings-panel-only; see FormField.environment
|
|
112
|
+
collapsible?: boolean; // starts collapsed behind a tappable `title` header; requires `title`
|
|
113
|
+
title?: string; // the tappable header shown when `collapsible` is set; ignored otherwise
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// A button that invokes a named hook method (e.g. 'clearCache') against the
|
|
117
|
+
// current form/panel state and shows the returned string as a result
|
|
118
|
+
// banner. `method` isn't statically declared anywhere — it's resolved
|
|
119
|
+
// against an extra, undeclared property on the registered hook object at
|
|
120
|
+
// dispatch time, the same "extra property beyond the typed interface" the
|
|
121
|
+
// SDK already permits (see settings.md's settingsPanel note). Not used for
|
|
122
|
+
// account credentials — those have their own testCredentials on AccountHook.
|
|
123
|
+
export interface FormActionElement {
|
|
124
|
+
type: 'action';
|
|
125
|
+
key: string;
|
|
126
|
+
label: string;
|
|
127
|
+
method: string;
|
|
128
|
+
devMode?: boolean;
|
|
129
|
+
common?: boolean; // settings-panel-only; see FormField.common
|
|
130
|
+
environment?: string | string[]; // settings-panel-only; see FormField.environment
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A row that opens an external URL (icon-row-styled tile with an
|
|
134
|
+
// open-in-new icon button); how it opens is app policy. Links carry no
|
|
135
|
+
// value and never appear in form state.
|
|
136
|
+
export interface FormLinkElement {
|
|
137
|
+
type: 'link';
|
|
138
|
+
key?: string;
|
|
139
|
+
label: string;
|
|
140
|
+
url: string;
|
|
141
|
+
icon?: string;
|
|
142
|
+
description?: string;
|
|
143
|
+
devMode?: boolean;
|
|
144
|
+
common?: boolean; // settings-panel-only; see FormField.common
|
|
145
|
+
environment?: string | string[]; // settings-panel-only; see FormField.environment
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type FormElement = FormField | FormHeader | FormSectionDivider | FormMarkdownBlock | FormActionElement | FormLinkElement;
|
|
149
|
+
|
|
150
|
+
export interface FormDefinition {
|
|
151
|
+
title?: string;
|
|
152
|
+
subtitle?: string;
|
|
153
|
+
elements: FormElement[];
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The same `FormDefinition`/`FormField` schema is also the basis for the settings panel system — see [settings.md](./settings.md).
|
|
158
|
+
|
|
159
|
+
## `devMode` elements
|
|
160
|
+
|
|
161
|
+
Any element — a field, a header, a divider, a markdown block, an action
|
|
162
|
+
button — can be marked `devMode: true`. It's then hidden entirely (not
|
|
163
|
+
rendered at all, not part of the submitted values) unless the app's developer
|
|
164
|
+
mode is on; when it is, the element renders normally but tinted in the same
|
|
165
|
+
orange accent app-polo uses for dev-mode-only settings (`styles.colors.devMode`
|
|
166
|
+
— label/icon/subtitle text color, no badge or border). This is host-driven —
|
|
167
|
+
the renderer is told whether developer mode is on, it doesn't check any
|
|
168
|
+
setting itself — so it works the same whether the form comes from an
|
|
169
|
+
extension's ad hoc `host.showForm` call or a `settingsPanel`.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Usage
|
|
174
|
+
|
|
175
|
+
### 1. Dialog Flow (`host.showForm`)
|
|
176
|
+
|
|
177
|
+
An extension can prompt the user at any time using a modal dialog:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { host } from "@ham2k/extension-sdk";
|
|
181
|
+
|
|
182
|
+
const result = await host.showForm({
|
|
183
|
+
title: "Setup Contest",
|
|
184
|
+
subtitle: "Configure exchange and operator options",
|
|
185
|
+
elements: [
|
|
186
|
+
{
|
|
187
|
+
type: "header",
|
|
188
|
+
title: "Station Parameters"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: "field",
|
|
192
|
+
fieldType: "callsign",
|
|
193
|
+
key: "operatorCall",
|
|
194
|
+
label: "Operator Callsign",
|
|
195
|
+
validate: (val) => val.length < 3 ? "Invalid callsign" : null,
|
|
196
|
+
transform: (val) => val.toUpperCase()
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: "divider"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: "field",
|
|
203
|
+
fieldType: "select",
|
|
204
|
+
key: "category",
|
|
205
|
+
label: "Contest Category",
|
|
206
|
+
options: [
|
|
207
|
+
{ label: "Single Operator Low Power", value: "SOLP" },
|
|
208
|
+
{ label: "Single Operator High Power", value: "SOHP" },
|
|
209
|
+
{ label: "Multi Operator", value: "MULTI" }
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}, {
|
|
214
|
+
operatorCall: "KI2D" // pre-filled state
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (result) {
|
|
218
|
+
console.log("Form values submitted:", result);
|
|
219
|
+
} else {
|
|
220
|
+
console.log("Form cancelled");
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### 2. View/Hook Flow (`form` category)
|
|
225
|
+
|
|
226
|
+
Extensions can also contribute static/predefined forms by registering a hook in the `form` category. The host can query the form by key, validate fields, and transform them:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// Static registration in extension code
|
|
230
|
+
api.registerHook('form', {
|
|
231
|
+
key: 'my-contest-settings',
|
|
232
|
+
hook: {
|
|
233
|
+
async getDefinition(args, ctx) {
|
|
234
|
+
return {
|
|
235
|
+
title: "Contest Setup",
|
|
236
|
+
elements: [
|
|
237
|
+
{
|
|
238
|
+
type: "field",
|
|
239
|
+
fieldType: "text",
|
|
240
|
+
key: "exchange",
|
|
241
|
+
label: "State/Section Exchange"
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
async validateField({ fieldKey, value, state }, ctx) {
|
|
247
|
+
if (fieldKey === 'exchange' && value.length !== 2) {
|
|
248
|
+
return "Exchange must be a 2-character abbreviation";
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
},
|
|
252
|
+
async transformField({ fieldKey, value, state }, ctx) {
|
|
253
|
+
if (fieldKey === 'exchange') {
|
|
254
|
+
return value.toUpperCase();
|
|
255
|
+
}
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Validation & Transformation Lifecycle
|
|
265
|
+
|
|
266
|
+
1. **User input**: When a user fills out text or makes selections, the Dart UI clears any previous error for that field.
|
|
267
|
+
2. **Submit validation**: When the user clicks **Save / Submit**:
|
|
268
|
+
- The Flutter form renderer triggers validation for each field asynchronously.
|
|
269
|
+
- Any validation error returns back to Dart and is displayed underneath the field. If any fields have errors, submission is aborted.
|
|
270
|
+
3. **Submit transformation**: When all fields are successfully validated, Dart invokes any JS transformation callbacks for each field.
|
|
271
|
+
4. **Completion**: The final transformed state is resolved and returned back to the caller (or saved in the view context).
|