@tiaforge-ui/sinter 0.1.0 → 0.3.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 (45) hide show
  1. package/README.md +39 -12
  2. package/dist/check/index.d.ts +4 -1
  3. package/dist/check/index.js +9 -8
  4. package/dist/check/shadow.d.ts +3 -1
  5. package/dist/check/shadow.js +53 -5
  6. package/dist/cli.js +2 -2
  7. package/dist/compiler/check-unit.d.ts +26 -2
  8. package/dist/compiler/compile.d.ts +4 -0
  9. package/dist/compiler/compile.js +314 -30
  10. package/dist/compiler/diagnostic-docs.js +20 -5
  11. package/dist/compiler/diagnostics.d.ts +3 -0
  12. package/dist/compiler/diagnostics.js +3 -0
  13. package/dist/compiler/expr.d.ts +24 -0
  14. package/dist/compiler/expr.js +53 -8
  15. package/dist/compiler/index.d.ts +3 -2
  16. package/dist/compiler/index.js +70 -16
  17. package/dist/compiler/vocabulary.d.ts +1 -0
  18. package/dist/compiler/vocabulary.js +9 -3
  19. package/dist/each.d.ts +3 -0
  20. package/dist/each.js +121 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/ir.d.ts +30 -1
  23. package/dist/listbox.d.ts +3 -0
  24. package/dist/listbox.js +66 -0
  25. package/dist/lsp/typescript.js +1 -1
  26. package/dist/lsp/workspace.js +3 -2
  27. package/dist/paginate.js +18 -4
  28. package/dist/project.d.ts +6 -0
  29. package/dist/project.js +22 -19
  30. package/dist/runtime/app.d.ts +35 -1
  31. package/dist/runtime/app.js +178 -40
  32. package/dist/runtime/config.d.ts +10 -3
  33. package/dist/runtime/config.js +10 -3
  34. package/dist/runtime/request.d.ts +1 -1
  35. package/dist/runtime/request.js +6 -1
  36. package/dist/runtime/store.d.ts +7 -4
  37. package/dist/runtime/store.js +154 -41
  38. package/dist/sse.js +8 -5
  39. package/dist/testing/index.d.ts +2 -0
  40. package/dist/testing/index.js +17 -2
  41. package/dist/vite.js +4 -2
  42. package/docs/diagnostics.md +35 -5
  43. package/html-custom-data.json +46 -2
  44. package/llms.txt +112 -6
  45. package/package.json +6 -1
@@ -12,13 +12,27 @@ export function clean(obj) {
12
12
  out[k] = obj[k];
13
13
  return out;
14
14
  }
15
+ /** One collator per locale: constructing one is costly in some engines. */
16
+ const collators = new Map();
17
+ const collator = (l = '') => collators.get(l) ?? (collators.set(l, new Intl.Collator(l || undefined, { numeric: true })), collators.get(l));
15
18
  export function createResource(name, o) {
16
19
  const meta = { loading: false, error: null, loadedAt: null, pending: 0, total: null };
17
20
  const coll = !!o.key, field = o.order?.replace(/^-/, ''), dir = o.order?.[0] == '-' ? -1 : 1;
18
21
  const subs = new Set();
19
22
  const prep = (x) => (o.prep ? o.prep(x) : x);
20
- let base = new Map(), single = null, total = null;
23
+ /** base items in arrival order, and each key's arrival number (ties in `order` keep arrival order) */
24
+ let base = new Map(), seq = new Map(), next = 0;
25
+ let single = null, total = null;
21
26
  let layers = [], lastLayer = 0;
27
+ /**
28
+ * The base in display order, the locale it was sorted for, and the keys written since, with
29
+ * their value before the first write. Reads patch it instead of sorting
30
+ * again. Arrays handed out are never changed afterwards.
31
+ */
32
+ let sorted = null, sortedFor, touched = new Map();
33
+ /** Optimistic layers applied over the base: changed keys (null: removed), keys they add (arrival numbers), removals counted for `total`. */
34
+ let over = null;
35
+ /** what readers see; rebuilt on the first read after a change */
22
36
  let view = null;
23
37
  let journal = null, loads = 0;
24
38
  const keyOf = (x) => typeof x == 'string' ? x : (o.key ?? []).map((f) => {
@@ -29,71 +43,171 @@ export function createResource(name, o) {
29
43
  const merge = (cur, op) => op.op == 'upsert' ? prep(op.item)
30
44
  : op.op == 'remove' ? null
31
45
  : cur && prep({ ...cur, ...clean(op.fields) });
32
- /** Applies one op to a keyed map; counts removals of present items. */
33
- const applyTo = (m, op, removed) => {
34
- const k = op.op == 'upsert' ? keyOf(op.item) : op.key, v = merge(m.get(k), op);
35
- if (v)
36
- m.set(k, v);
37
- else if (m.delete(k) && removed)
38
- removed.n++;
46
+ /** Applies one op to the base. */
47
+ const apply = (op) => {
48
+ if (!coll)
49
+ return void (single = merge(single, op));
50
+ const k = op.op == 'upsert' ? keyOf(op.item) : op.key, cur = base.get(k), v = merge(cur, op);
51
+ if (sorted && !touched.has(k))
52
+ touched.set(k, cur);
53
+ if (v) {
54
+ if (!cur)
55
+ seq.set(k, next++);
56
+ base.set(k, v);
57
+ }
58
+ else if (cur) {
59
+ base.delete(k);
60
+ seq.delete(k);
61
+ }
39
62
  };
40
- const changed = (what = 'data') => {
63
+ /** By `order` (none: all equal, so arrival order decides). */
64
+ const compare = (a, b) => {
65
+ if (!field)
66
+ return 0;
67
+ const x = a[field], y = b[field];
68
+ if (x == null || y == null)
69
+ return +(x == null) - +(y == null);
70
+ return dir * (typeof x == 'string' && typeof y == 'string' ? collator(sortedFor).compare(x, y) : +(x > y) - +(x < y));
71
+ };
72
+ /** A sorted copy of `list` without the `drop` items and with the `add` items (each with its arrival number). */
73
+ function patch(list, drop, add, arrival) {
74
+ const out = list.slice();
75
+ /** the first index whose item does not sort before `v` (with arrival number `n`, after its ties) */
76
+ const find = (v, n) => {
77
+ let lo = 0, hi = out.length;
78
+ while (lo < hi) {
79
+ const mid = (lo + hi) >> 1, c = compare(out[mid], v) || (n == null ? 0 : arrival(keyOf(out[mid])) - n);
80
+ c < 0 ? (lo = mid + 1) : (hi = mid);
81
+ }
82
+ return lo;
83
+ };
84
+ for (const v of drop) {
85
+ // among its ties, by identity; an item whose order field was changed in place is found by a scan
86
+ let i = find(v);
87
+ while (out[i] !== v && i < out.length && !compare(out[i], v))
88
+ i++;
89
+ out.splice(out[i] === v ? i : out.indexOf(v), 1);
90
+ }
91
+ for (const [v, n] of add)
92
+ out.splice(find(v, n), 0, v);
93
+ return out;
94
+ }
95
+ /** The base in display order. */
96
+ function baseList() {
97
+ const locale = o.locale?.();
98
+ if (sorted && locale === sortedFor && touched.size <= 64) {
99
+ if (touched.size) {
100
+ const drop = [], add = [];
101
+ for (const [k, old] of touched) {
102
+ if (old)
103
+ drop.push(old);
104
+ const v = base.get(k);
105
+ if (v)
106
+ add.push([v, seq.get(k)]);
107
+ }
108
+ sorted = patch(sorted, drop, add, (k) => seq.get(k));
109
+ }
110
+ }
111
+ else {
112
+ sortedFor = locale;
113
+ // a stable sort of the Map's arrival order
114
+ sorted = [...base.values()].sort(compare);
115
+ }
116
+ touched = new Map();
117
+ return sorted;
118
+ }
119
+ /** Recomputes the layers' effect and `meta.total` after any change; marks `meta` if the total changed. */
120
+ function sync() {
121
+ over = null;
122
+ if (layers.length) {
123
+ const m = new Map(), add = new Map();
124
+ let removed = 0, n = next, s = single;
125
+ for (const l of layers)
126
+ for (const op of l.ops) {
127
+ if (!coll) {
128
+ s = merge(s, op);
129
+ continue;
130
+ }
131
+ const k = op.op == 'upsert' ? keyOf(op.item) : op.key, cur = m.has(k) ? m.get(k) : base.get(k), v = merge(cur, op);
132
+ if (v) {
133
+ if (!cur)
134
+ add.set(k, n++);
135
+ m.set(k, v);
136
+ }
137
+ else {
138
+ if (cur)
139
+ removed++;
140
+ m.set(k, null);
141
+ add.delete(k);
142
+ }
143
+ }
144
+ over = { m, add, removed, s };
145
+ }
146
+ const t = total == null ? null : Math.max(0, total - (over?.removed ?? 0));
147
+ if (meta.total !== t) {
148
+ meta.total = t;
149
+ r.onDirty('meta');
150
+ }
151
+ }
152
+ /** A data change: readers rebuild their view; `meta` too when the pending count changed. */
153
+ const changed = (pending) => {
41
154
  view = null;
155
+ sync();
42
156
  r.onDirty('data');
43
- if (what == 'meta')
157
+ if (pending) {
158
+ meta.pending = layers.length;
44
159
  r.onDirty('meta');
160
+ }
45
161
  };
46
162
  function visible() {
47
163
  if (view)
48
164
  return view;
49
- let t = total, m = base, s = single;
50
- const removed = { n: 0 };
51
- for (const l of layers)
52
- for (const op of l.ops)
53
- coll ? (m == base && (m = new Map(base)), applyTo(m, op, removed)) : (s = merge(s, op));
54
- if (t != null)
55
- t = Math.max(0, t - removed.n);
56
- const l = [...m.values()];
57
- if (field) {
58
- const c = new Intl.Collator(o.locale?.(), { numeric: true });
59
- l.sort((a, b) => {
60
- const x = a[field], y = b[field];
61
- if (x == null || y == null)
62
- return +(x == null) - +(y == null);
63
- return dir * (typeof x == 'string' && typeof y == 'string' ? c.compare(x, y) : +(x > y) - +(x < y));
64
- });
65
- }
66
- view = { l: Object.freeze(l), m, s };
67
- if (meta.total !== t) {
68
- meta.total = t;
69
- r.onDirty('meta');
165
+ if (!coll)
166
+ return (view = { l: [], get: () => undefined, s: over ? over.s : single });
167
+ let l = baseList(), get = (k) => base.get(k);
168
+ if (over) {
169
+ const { m, add } = over, drop = [], put = [], at = (k) => add.get(k) ?? seq.get(k);
170
+ for (const [k, v] of m) {
171
+ const b = base.get(k);
172
+ if (b)
173
+ drop.push(b);
174
+ if (v)
175
+ put.push([v, at(k)]);
176
+ }
177
+ l = patch(l, drop, put, at);
178
+ get = (k) => (m.has(k) ? m.get(k) ?? undefined : base.get(k));
70
179
  }
71
- return view;
180
+ // dev builds freeze what expressions see; the compiler refuses writes to resources (SN205)
181
+ return (view = { l: o.freeze ? Object.freeze(l.slice()) : l, get, s: null });
72
182
  }
73
183
  function write(op) {
74
184
  if (r.recording)
75
185
  return void r.recording.push(op);
76
186
  journal?.push(op);
77
- coll ? applyTo(base, op) : (single = merge(single, op));
187
+ apply(op);
78
188
  changed();
79
189
  for (const f of subs)
80
190
  f({ resource: name, ...op });
81
191
  }
82
192
  const r = {
83
193
  name, meta, key: o.key, isCollection: coll, keyOf, recording: null, loader: null, onDirty() { },
84
- get: (k) => (coll ? visible().m.get(keyOf(k)) : visible().s ?? undefined),
194
+ get: (k) => (coll ? visible().get(keyOf(k)) : visible().s ?? undefined),
85
195
  has: (k) => r.get(k) !== undefined,
86
196
  all: () => visible().l,
87
197
  value: () => (coll ? visible().l : visible().s),
88
198
  replace(items) {
89
199
  if (coll) {
90
200
  base = new Map();
201
+ seq = new Map();
202
+ sorted = null;
203
+ touched = new Map();
91
204
  for (const x of Array.isArray(items) ? items : []) {
92
205
  const k = keyOf(x);
93
206
  if (base.has(k))
94
207
  o.warn?.(13, name, k);
95
208
  base.delete(k);
96
209
  base.set(k, prep(x));
210
+ seq.set(k, next++);
97
211
  }
98
212
  }
99
213
  else
@@ -105,7 +219,7 @@ export function createResource(name, o) {
105
219
  upsert: (item) => write({ op: 'upsert', item }),
106
220
  patch: (k, fields) => write({ op: 'patch', key: keyOf(k), fields }),
107
221
  remove: (k) => write({ op: 'remove', key: keyOf(k) }),
108
- refetch: () => r.loader?.refetch() ?? Promise.resolve(),
222
+ refetch: (hint) => r.loader?.refetch(hint) ?? Promise.resolve(),
109
223
  subscribe: (fn) => (subs.add(fn), () => subs.delete(fn)),
110
224
  startLoad() {
111
225
  loads++;
@@ -119,23 +233,22 @@ export function createResource(name, o) {
119
233
  if (!loaded || !ops.length)
120
234
  return;
121
235
  for (const op of ops)
122
- coll ? applyTo(base, op) : (single = merge(single, op));
236
+ apply(op);
123
237
  changed();
124
238
  },
125
239
  addLayer(ops) {
126
240
  layers.push({ id: ++lastLayer, ops });
127
- meta.pending = layers.length;
128
- changed('meta');
241
+ changed(true);
129
242
  return lastLayer;
130
243
  },
131
244
  dropLayer(id) {
132
245
  const n = layers.length;
133
246
  layers = layers.filter((l) => l.id != id);
134
- meta.pending = layers.length;
135
247
  if (n != layers.length)
136
- changed('meta');
248
+ changed(true);
137
249
  },
138
- setTotal(t) { total = t; changed(); },
250
+ /** the total changes `meta` only: bindings that read the items do not re-run */
251
+ setTotal(t) { total = t; sync(); },
139
252
  base: () => ({ items: coll ? [...base.values()] : single, total }),
140
253
  };
141
254
  return r;
package/dist/sse.js CHANGED
@@ -45,10 +45,13 @@ class Connection {
45
45
  everOpen = false;
46
46
  url;
47
47
  fed;
48
+ /** where `src` and the events' expressions are evaluated: the enclosing state's scope, or the page */
49
+ base;
48
50
  constructor(app, def) {
49
51
  this.app = app;
50
52
  this.def = def;
51
- this.url = app.url(def.src, app.page);
53
+ this.base = app.scopeOf(def.scope ?? -1);
54
+ this.url = app.url(def.src, this.base);
52
55
  this.fed = [...new Set(def.events.map((e) => e.resource))];
53
56
  }
54
57
  start() {
@@ -116,7 +119,7 @@ class Connection {
116
119
  status = (await this.app.req.raw(config.sessionProbe)).status;
117
120
  }
118
121
  catch (e) {
119
- status = e instanceof SinterError ? e.status : 0;
122
+ status = e instanceof SinterError ? e.status ?? 0 : 0;
120
123
  }
121
124
  if (this.closed || this.hidden)
122
125
  return;
@@ -139,7 +142,7 @@ class Connection {
139
142
  }
140
143
  apply(def, payload) {
141
144
  const app = this.app;
142
- const scope = Object.create(app.page);
145
+ const scope = Object.create(this.base);
143
146
  scope.$p = payload && typeof payload === 'object' ? payload : {};
144
147
  const r = app.resource(def.resource);
145
148
  try {
@@ -148,13 +151,13 @@ class Connection {
148
151
  const paginated = !!app.ir.resources.find((x) => x.name === def.resource)?.paginate;
149
152
  switch (def.op) {
150
153
  case 'refetch':
151
- void r.refetch();
154
+ void r.refetch(scope.$p);
152
155
  return;
153
156
  case 'upsert': {
154
157
  const item = def.item != null ? app.fns[def.item](scope) : payload;
155
158
  if (item == null || typeof item !== 'object') {
156
159
  if (def.fallback === 'refetch')
157
- void r.refetch();
160
+ void r.refetch(scope.$p);
158
161
  return;
159
162
  }
160
163
  if (paginated && !r.has(item)) {
@@ -44,6 +44,8 @@ export interface RenderOptions {
44
44
  now?: number | Date;
45
45
  /** helper functions, and namespaces as objects of functions */
46
46
  helpers?: Record<string, unknown>;
47
+ /** action functions for `data-sn-call` and `<sn-resource call>`, and namespaces as objects of functions */
48
+ actions?: Record<string, unknown>;
47
49
  /** partials for `<sn-include>`, by path relative to the page */
48
50
  files?: Record<string, string>;
49
51
  /** localStorage and sessionStorage (default: empty ones); pass the previous page's to test what carries over */
@@ -11,9 +11,11 @@ import cache from '../cache.js';
11
11
  import dev from '../dev.js';
12
12
  import optimistic from '../optimistic.js';
13
13
  import paginate from '../paginate.js';
14
+ import each from '../each.js';
14
15
  import persist from '../persist.js';
16
+ import listbox from '../listbox.js';
15
17
  import sse from '../sse.js';
16
- const FEATURES = { dev, sse, optimistic, paginate, persist, cache };
18
+ const FEATURES = { dev, sse, optimistic, paginate, persist, cache, each, listbox };
17
19
  function isResponseSpec(v) {
18
20
  if (!v || typeof v !== 'object' || Array.isArray(v))
19
21
  return false;
@@ -172,9 +174,17 @@ export async function render(html, options = {}) {
172
174
  else if (v && typeof v === 'object')
173
175
  manifest.namespaces[k] = { module: k, members: Object.keys(v) };
174
176
  }
177
+ const actions = options.actions ?? {};
178
+ const actionManifest = { module: 'actions', names: [], namespaces: {} };
179
+ for (const [k, v] of Object.entries(actions)) {
180
+ if (typeof v === 'function')
181
+ actionManifest.names.push(k);
182
+ else if (v && typeof v === 'object')
183
+ actionManifest.namespaces[k] = { module: k, members: Object.keys(v) };
184
+ }
175
185
  const files = options.files ?? {};
176
186
  const parts = compileParts(html, {
177
- file: PAGE, root: '/test', dev: true, helpers: manifest, moduleSrc: 'about:blank',
187
+ file: PAGE, root: '/test', dev: true, helpers: manifest, actions: actionManifest, moduleSrc: 'about:blank',
178
188
  readFile: (p) => {
179
189
  const rel = p.startsWith('/test/') ? p.slice('/test/'.length) : p;
180
190
  const hit = files[rel] ?? files[`./${rel}`] ?? files[p];
@@ -203,6 +213,11 @@ export async function render(html, options = {}) {
203
213
  values.push(helpers[ns][m]);
204
214
  }
205
215
  }
216
+ for (const c of parts.calls) {
217
+ const [a, b] = c.split('.');
218
+ names.push(`$a$${c.replace('.', '$')}`);
219
+ values.push(b === undefined ? actions[a] : actions[a][b]);
220
+ }
206
221
  // eslint-disable-next-line no-new-func -- tests only; pages load a compiled module
207
222
  const fns = new Function(...names, `"use strict"; return [\n${parts.fns.join(',\n')}\n];`)(...values);
208
223
  // the page
package/dist/vite.js CHANGED
@@ -51,7 +51,7 @@ export default function sinter(options = {}) {
51
51
  return;
52
52
  const { check } = await import('./check/index.js');
53
53
  const p = project(root, options);
54
- const r = check({ root, units: [...units.values()], helpers: p.helpers, types: p.types, openapi: p.openapi });
54
+ const r = check({ root, units: [...units.values()], helpers: p.helpers, actions: p.actions, types: p.types, openapi: p.openapi });
55
55
  units.clear();
56
56
  if (r.diagnostics.length)
57
57
  this.error(`sinter check: ${r.diagnostics.length} type error(s)\n${r.diagnostics.map(formatDiagnostic).join('\n')}`);
@@ -66,7 +66,7 @@ export default function sinter(options = {}) {
66
66
  if (p.diagnostics.length)
67
67
  throw new Error(`sinter: ${p.diagnostics.map(formatDiagnostic).join('\n')}`);
68
68
  const out = compile(html, {
69
- file, root, dev, helpers: p.helpers, moduleSrc, config: p.config, readFile: p.readFile,
69
+ file, root, dev, helpers: p.helpers, actions: p.actions, moduleSrc, config: p.config, readFile: p.readFile,
70
70
  check: !!options.typecheck && !dev, openapi: p.openapi,
71
71
  });
72
72
  if (out.check)
@@ -75,6 +75,8 @@ export default function sinter(options = {}) {
75
75
  watch(f);
76
76
  if (p.helpersFile)
77
77
  watch(p.helpersFile);
78
+ if (p.actionsFile)
79
+ watch(p.actionsFile);
78
80
  const errors = out.diagnostics.filter((d) => d.severity === 'error');
79
81
  for (const d of out.diagnostics)
80
82
  if (d.severity === 'warning')
@@ -122,6 +122,16 @@ TypeScript found a type error in an expression. Reported by `sinter check`, `sin
122
122
 
123
123
  **Fix:** Fix the expression, or the type: `type` on `<sn-resource>`, the OpenAPI spec, or a JSDoc cast in `data-sn-state`.
124
124
 
125
+ ## SN209 name-conflict
126
+
127
+ Error, for example:
128
+
129
+ > `s` is already defined here by `data-sn-let`; use another name
130
+
131
+ A `data-sn-let` name is inlined wherever it is read, so it cannot share a name with a state variable or loop variable in its scope, or be redeclared inside it.
132
+
133
+ **Fix:** Rename the let, or the state or loop variable.
134
+
125
135
  ## SN301 unknown-resource
126
136
 
127
137
  Error, for example:
@@ -148,7 +158,7 @@ Error, for example:
148
158
 
149
159
  > `$meta.contacts.loadng` is not a status field. Did you mean `loading`?
150
160
 
151
- `$meta.<resource>` has a fixed set of fields: `loading`, `error`, `loadedAt`, `pending`, `total`, and with `paginate` also `page`, `pages`, `from`, `to`, `hasPrev`, `hasNext`.
161
+ `$meta.<resource>` has a fixed set of fields: `loading`, `error`, `loadedAt`, `pending`, `total`, and with `paginate` also `pages`, `from`, `to`, `hasPrev`, `hasNext`; `page` with `paginate="offset"`, or keyset with `offset-param`.
152
162
 
153
163
  **Fix:** Use the suggested field. A pagination field needs `paginate` on the resource.
154
164
 
@@ -156,11 +166,11 @@ Error, for example:
156
166
 
157
167
  Error, for example:
158
168
 
159
- > keyset pagination cannot jump to a page number. Use `paginate="offset"`
169
+ > keyset pagination cannot jump to a page number. Add `offset-param` if the server also accepts an offset, or use `paginate="offset"`
160
170
 
161
- Keyset (cursor) pagination only knows the next and previous page, so `page('name', 7)` has nothing to jump to.
171
+ Keyset (cursor) pagination only knows the next and previous page, so `page('name', 7)` has nothing to jump to, unless the server can also skip items by offset.
162
172
 
163
- **Fix:** Use `paginate="offset"` if the API supports offsets, or only step with `next`, `prev` and `first`.
173
+ **Fix:** If the API accepts an offset next to its cursor, add `offset-param="offset"`: the jump is sent as an offset and `prev`/`next` continue by cursor. Otherwise use `paginate="offset"`, or only step with `first`, `prev`, `next` and `last`.
164
174
 
165
175
  ## SN305 include-not-found
166
176
 
@@ -202,6 +212,26 @@ The resource matched an operation in the OpenAPI spec, but its `items` or `total
202
212
 
203
213
  **Fix:** Fix the path in `items` or `total`, or the spec if the API changed.
204
214
 
215
+ ## SN309 unknown-dialog
216
+
217
+ Error, for example:
218
+
219
+ > no `<dialog id="compose-dialg">` on this page. Did you mean `compose-dialog`?
220
+
221
+ `showModal(id)` and `close(id)` name a `<dialog>` by its literal `id`, which must be on the page or one of its partials.
222
+
223
+ **Fix:** Fix the id, or add `id` to the `<dialog>`. `close()` without an argument closes the dialog around the element.
224
+
225
+ ## SN310 unknown-action
226
+
227
+ Error, for example:
228
+
229
+ > `pgp.decrpyt` is not exported by the actions module. Did you mean `pgp.decrypt`?
230
+
231
+ `data-sn-call` and `<sn-resource call>` name a function the actions module (`src/sinter.actions.js` or `.ts`) exports, directly or through `export * as ns`.
232
+
233
+ **Fix:** Use the suggested name, or export the function from the actions module.
234
+
205
235
  ## SN401 parse-error
206
236
 
207
237
  Error, for example:
@@ -250,7 +280,7 @@ Warning, for example:
250
280
 
251
281
  The type checker has no type for the resource, so expressions that use it are not checked. Reported by `sinter check` only.
252
282
 
253
- **Fix:** Add `type="Contact[]"` with a type from `src/sinter.types.ts`, configure an OpenAPI spec that matches `src`, or write `type="any"` to opt out.
283
+ **Fix:** Add `type="Contact[]"` with a type from `src/sinter.types.ts`, configure an OpenAPI spec that matches `src`, load it with `call` (typed by what the action returns), or write `type="any"` to opt out.
254
284
 
255
285
  ## SN505 unknown-query-param
256
286
 
@@ -11,6 +11,12 @@
11
11
  {
12
12
  "name": "src"
13
13
  },
14
+ {
15
+ "name": "call"
16
+ },
17
+ {
18
+ "name": "each"
19
+ },
14
20
  {
15
21
  "name": "key"
16
22
  },
@@ -41,9 +47,15 @@
41
47
  {
42
48
  "name": "direction-param"
43
49
  },
50
+ {
51
+ "name": "direction-values"
52
+ },
44
53
  {
45
54
  "name": "page-param"
46
55
  },
56
+ {
57
+ "name": "offset-param"
58
+ },
47
59
  {
48
60
  "name": "type"
49
61
  },
@@ -106,6 +118,10 @@
106
118
  "name": "data-sn-state",
107
119
  "description": "declares local reactive state for the element and its subtree (object literal)"
108
120
  },
121
+ {
122
+ "name": "data-sn-let",
123
+ "description": "derived names for the element and its subtree, computed where they are read; later names may use earlier ones (object literal {name: expression})"
124
+ },
109
125
  {
110
126
  "name": "data-sn-text",
111
127
  "description": "sets textContent; never HTML (value expression)"
@@ -150,6 +166,10 @@
150
166
  "name": "data-sn-else",
151
167
  "description": "on a <template> directly after an if template"
152
168
  },
169
+ {
170
+ "name": "data-sn-listbox",
171
+ "description": "on a text input: arrow keys mark a [role=option] of that list (empty: the nearest [role=listbox] around the input), Enter or Tab clicks it, Escape fires `dismiss` (id of the suggestion list, or empty)"
172
+ },
153
173
  {
154
174
  "name": "data-sn-persist",
155
175
  "description": "persists the model or state in localStorage under sn: (storage key)"
@@ -174,6 +194,10 @@
174
194
  "name": "data-sn-delete",
175
195
  "description": "DELETE action (URL template)"
176
196
  },
197
+ {
198
+ "name": "data-sn-call",
199
+ "description": "local action: calls a function from the actions module, e.g. pgp.decrypt(msg) (action call)"
200
+ },
177
201
  {
178
202
  "name": "data-sn-body",
179
203
  "description": "JSON request body; overrides form serialization (value expression)"
@@ -184,7 +208,7 @@
184
208
  },
185
209
  {
186
210
  "name": "data-sn-debounce",
187
- "description": "delays the trigger, resets on each event (milliseconds)"
211
+ "description": "delays the trigger, resets on each event; without a method, delays the element's data-sn-on-* handlers (milliseconds)"
188
212
  },
189
213
  {
190
214
  "name": "data-sn-concurrency",
@@ -200,7 +224,15 @@
200
224
  },
201
225
  {
202
226
  "name": "data-sn-catch",
203
- "description": "runs after an error; $error = {status, body, message} (handler expression)"
227
+ "description": "runs after an error; $error = {status, body, message, detail} (handler expression)"
228
+ },
229
+ {
230
+ "name": "data-sn-confirm",
231
+ "description": "asks this question before the action runs (configure({ confirm }), window.confirm by default); no question when falsy (value expression)"
232
+ },
233
+ {
234
+ "name": "data-sn-encoding",
235
+ "description": "form: the body (data-sn-body, arrays as repeated keys, or the form) goes as application/x-www-form-urlencoded (json | form)"
204
236
  },
205
237
  {
206
238
  "name": "data-sn-path",
@@ -229,6 +261,18 @@
229
261
  {
230
262
  "name": "data-sn-on-keydown",
231
263
  "description": "plain event handler, no request; e.g. data-sn-on-click"
264
+ },
265
+ {
266
+ "name": "data-sn-debounce-input",
267
+ "description": "delays only that event's data-sn-on-<event> handler (it runs with the last event); e.g. data-sn-debounce-input"
268
+ },
269
+ {
270
+ "name": "data-sn-debounce-keyup",
271
+ "description": "delays only that event's data-sn-on-<event> handler (it runs with the last event); e.g. data-sn-debounce-input"
272
+ },
273
+ {
274
+ "name": "data-sn-debounce-scroll",
275
+ "description": "delays only that event's data-sn-on-<event> handler (it runs with the last event); e.g. data-sn-debounce-input"
232
276
  }
233
277
  ]
234
278
  }