@simai-studio/view 0.0.0-stage → 1.1.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 (127) hide show
  1. package/LICENSE.md +183 -0
  2. package/README.md +75 -2
  3. package/dist/package-receipt.json +972 -0
  4. package/dist/set.json +141 -0
  5. package/dist/smart/rule.js +278 -0
  6. package/dist/smart/rule.js.gz +0 -0
  7. package/dist/smart/view/aliases.json +129 -0
  8. package/dist/smart/view/aliases.json.gz +0 -0
  9. package/dist/smart/view/component.json +1628 -0
  10. package/dist/smart/view/component.json.gz +0 -0
  11. package/dist/smart/view/css/view.css +208 -0
  12. package/dist/smart/view/css/view.css.gz +0 -0
  13. package/dist/smart/view/css/view.min.css +2 -0
  14. package/dist/smart/view/css/view.min.css.gz +0 -0
  15. package/dist/smart/view/examples.json +668 -0
  16. package/dist/smart/view/examples.json.gz +0 -0
  17. package/dist/smart/view/js/view.js +3955 -0
  18. package/dist/smart/view/js/view.js.gz +0 -0
  19. package/dist/smart/view/js/view.min.js +1 -0
  20. package/dist/smart/view/js/view.min.js.gz +0 -0
  21. package/dist/smart/view/lang/en.json +61 -0
  22. package/dist/smart/view/lang/en.json.gz +0 -0
  23. package/dist/smart/view/lang/ru.json +61 -0
  24. package/dist/smart/view/lang/ru.json.gz +0 -0
  25. package/dist/smart/view/probe.json +165 -0
  26. package/dist/smart/view/probe.json.gz +0 -0
  27. package/dist/smart/view/smart.manifest.json +1577 -0
  28. package/dist/smart/view/smart.manifest.json.gz +0 -0
  29. package/dist/smart/view/static/default/embedded.html +96 -0
  30. package/dist/smart/view/static/default/embedded.html.gz +0 -0
  31. package/dist/smart/view/static/default/open-record.html +242 -0
  32. package/dist/smart/view/static/default/open-record.html.gz +0 -0
  33. package/dist/smart/view/static/default/reference.html +242 -0
  34. package/dist/smart/view/static/default/reference.html.gz +0 -0
  35. package/dist/smart/view/static/sheets.json +282 -0
  36. package/dist/smart/view/static/sheets.json.gz +0 -0
  37. package/dist/smart/view-properties/aliases.json +174 -0
  38. package/dist/smart/view-properties/aliases.json.gz +0 -0
  39. package/dist/smart/view-properties/component.json +1906 -0
  40. package/dist/smart/view-properties/component.json.gz +0 -0
  41. package/dist/smart/view-properties/css/view-properties.css +1158 -0
  42. package/dist/smart/view-properties/css/view-properties.css.gz +0 -0
  43. package/dist/smart/view-properties/css/view-properties.min.css +2 -0
  44. package/dist/smart/view-properties/css/view-properties.min.css.gz +0 -0
  45. package/dist/smart/view-properties/examples.json +841 -0
  46. package/dist/smart/view-properties/examples.json.gz +0 -0
  47. package/dist/smart/view-properties/js/view-properties.js +10551 -0
  48. package/dist/smart/view-properties/js/view-properties.js.gz +0 -0
  49. package/dist/smart/view-properties/js/view-properties.min.js +1392 -0
  50. package/dist/smart/view-properties/js/view-properties.min.js.gz +0 -0
  51. package/dist/smart/view-properties/lang/en.json +89 -0
  52. package/dist/smart/view-properties/lang/en.json.gz +0 -0
  53. package/dist/smart/view-properties/lang/ru.json +89 -0
  54. package/dist/smart/view-properties/lang/ru.json.gz +0 -0
  55. package/dist/smart/view-properties/probe.json +188 -0
  56. package/dist/smart/view-properties/probe.json.gz +0 -0
  57. package/dist/smart/view-properties/skeleton.html +13 -0
  58. package/dist/smart/view-properties/skeleton.html.gz +0 -0
  59. package/dist/smart/view-properties/smart.manifest.json +1593 -0
  60. package/dist/smart/view-properties/smart.manifest.json.gz +0 -0
  61. package/dist/smart/view-properties/static/default/full.html +2 -0
  62. package/dist/smart/view-properties/static/default/full.html.gz +0 -0
  63. package/dist/smart/view-properties/static/default/panel-closed.html +4 -0
  64. package/dist/smart/view-properties/static/default/panel-closed.html.gz +0 -0
  65. package/dist/smart/view-properties/static/default/panel.html +4 -0
  66. package/dist/smart/view-properties/static/default/panel.html.gz +0 -0
  67. package/dist/smart/view-properties/static/default/reference.html +2 -0
  68. package/dist/smart/view-properties/static/default/reference.html.gz +0 -0
  69. package/dist/smart/view-properties/static/sheets.json +426 -0
  70. package/dist/smart/view-properties/static/sheets.json.gz +0 -0
  71. package/dist/smart/view-properties/template/default.css +992 -0
  72. package/dist/smart/view-properties/template/default.css.gz +0 -0
  73. package/dist/smart/view-properties/template/default.js +1658 -0
  74. package/dist/smart/view-properties/template/default.js.gz +0 -0
  75. package/dist/smart/view-table/aliases.json +448 -0
  76. package/dist/smart/view-table/aliases.json.gz +0 -0
  77. package/dist/smart/view-table/component.json +2746 -0
  78. package/dist/smart/view-table/component.json.gz +0 -0
  79. package/dist/smart/view-table/css/view-table.css +3658 -0
  80. package/dist/smart/view-table/css/view-table.css.gz +0 -0
  81. package/dist/smart/view-table/css/view-table.min.css +2 -0
  82. package/dist/smart/view-table/css/view-table.min.css.gz +0 -0
  83. package/dist/smart/view-table/examples.json +499 -0
  84. package/dist/smart/view-table/examples.json.gz +0 -0
  85. package/dist/smart/view-table/js/view-table.js +14166 -0
  86. package/dist/smart/view-table/js/view-table.js.gz +0 -0
  87. package/dist/smart/view-table/js/view-table.min.js +1440 -0
  88. package/dist/smart/view-table/js/view-table.min.js.gz +0 -0
  89. package/dist/smart/view-table/lang/en.json +132 -0
  90. package/dist/smart/view-table/lang/en.json.gz +0 -0
  91. package/dist/smart/view-table/lang/ru.json +132 -0
  92. package/dist/smart/view-table/lang/ru.json.gz +0 -0
  93. package/dist/smart/view-table/probe.json +275 -0
  94. package/dist/smart/view-table/probe.json.gz +0 -0
  95. package/dist/smart/view-table/smart.manifest.json +2531 -0
  96. package/dist/smart/view-table/smart.manifest.json.gz +0 -0
  97. package/dist/smart/view-table/static/default/current.html +141 -0
  98. package/dist/smart/view-table/static/default/current.html.gz +0 -0
  99. package/dist/smart/view-table/static/default/current.skeleton.html +141 -0
  100. package/dist/smart/view-table/static/default/current.skeleton.html.gz +0 -0
  101. package/dist/smart/view-table/static/default/empty.html +149 -0
  102. package/dist/smart/view-table/static/default/empty.html.gz +0 -0
  103. package/dist/smart/view-table/static/default/loading.html +180 -0
  104. package/dist/smart/view-table/static/default/loading.html.gz +0 -0
  105. package/dist/smart/view-table/static/default/many.html +180 -0
  106. package/dist/smart/view-table/static/default/many.html.gz +0 -0
  107. package/dist/smart/view-table/static/default/reference.html +136 -0
  108. package/dist/smart/view-table/static/default/reference.html.gz +0 -0
  109. package/dist/smart/view-table/static/default/reference.skeleton.html +136 -0
  110. package/dist/smart/view-table/static/default/reference.skeleton.html.gz +0 -0
  111. package/dist/smart/view-table/static/default/typed-cells.html +229 -0
  112. package/dist/smart/view-table/static/default/typed-cells.html.gz +0 -0
  113. package/dist/smart/view-table/static/default/typed-cells.skeleton.html +229 -0
  114. package/dist/smart/view-table/static/default/typed-cells.skeleton.html.gz +0 -0
  115. package/dist/smart/view-table/static/sheets.json +480 -0
  116. package/dist/smart/view-table/static/sheets.json.gz +0 -0
  117. package/dist/smart/view-table/studio.fixture.json +17 -0
  118. package/dist/smart/view-table/studio.fixture.json.gz +0 -0
  119. package/dist/smart/view-table/studio.scenario.json +30 -0
  120. package/dist/smart/view-table/studio.scenario.json.gz +0 -0
  121. package/dist/smart/view-table/template/default.css +1301 -0
  122. package/dist/smart/view-table/template/default.css.gz +0 -0
  123. package/dist/smart/view-table/template/default.js +1215 -0
  124. package/dist/smart/view-table/template/default.js.gz +0 -0
  125. package/dist/smart/view-table/template/link-cell-contract.js +48 -0
  126. package/dist/smart/view-table/template/link-cell-contract.js.gz +0 -0
  127. package/package.json +29 -4
@@ -0,0 +1,3955 @@
1
+ /******/ (() => { // webpackBootstrap
2
+ /******/ "use strict";
3
+ /******/ var __webpack_modules__ = ({
4
+
5
+ /***/ "51250bf01ab0"
6
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
7
+
8
+ __webpack_require__.r(__webpack_exports__);
9
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
10
+ /* harmony export */ createPreferences: () => (/* binding */ createPreferences),
11
+ /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
12
+ /* harmony export */ });
13
+ // SF.Preferences: what a person changes about how a thing looks -- column
14
+ // order, widths, sort, density, a menu's column widths -- kept one way for
15
+ // every component (owner, 2026-10-14; ui-control
16
+ // control/designs/2026-10-14-view-preferences.md).
17
+ //
18
+ // - A preference is a plain JSON value under a key `<component>/<instance>`,
19
+ // e.g. `sf-table/cms.solutions`. No key, nothing is kept.
20
+ // - It is read synchronously, so a component applies it before its first
21
+ // paint: the skeleton is already in the person's order.
22
+ // - The browser keeps a copy always (localStorage `sf-pref:<key>`), the cache
23
+ // a first paint can read. A host that keeps preferences for its user
24
+ // (Larena) plugs in with useHost(); its values win over the cache, and every
25
+ // write goes to it as well, debounced per key.
26
+ // - A page can preload the host's values: preload({key: value}), or a
27
+ // <script type="application/json" data-sf-preferences> (or id
28
+ // "sf-preferences") in the page, read before the first value is.
29
+ //
30
+ // The registry lives on globalThis.SF: every bundle carries its own copy of
31
+ // this module, and two copies with two caches would disagree.
32
+ const PREFIX = 'sf-pref:';
33
+ const DELAY = 400;
34
+ const KEY = /^[\w.*:-]+(?:\/[\w.*:/-]+)?$/u;
35
+
36
+ function storage() {
37
+ try {
38
+ return globalThis.localStorage ?? null;
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function validKey(key) {
45
+ return typeof key === 'string' && key.length > 0 && key.length <= 200 && KEY.test(key);
46
+ }
47
+
48
+ function same(a, b) {
49
+ try {
50
+ return JSON.stringify(a) === JSON.stringify(b);
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function plain(value) {
57
+ return value === null || value === undefined ? null : JSON.parse(JSON.stringify(value));
58
+ }
59
+
60
+ function createPreferences() {
61
+ const host = {
62
+ adapter: null,
63
+ values: new Map()
64
+ };
65
+ const listeners = new Map();
66
+ const timers = new Map(); // The page's own block of the host's values, read once, before the first
67
+ // read: a component may connect before this module's start-up ran.
68
+
69
+ let inlineRead = false;
70
+
71
+ const readInline = () => {
72
+ if (inlineRead) return;
73
+ const node = globalThis.document?.querySelector?.('script[type="application/json"][data-sf-preferences], script#sf-preferences[type="application/json"]');
74
+ if (!node) return;
75
+ inlineRead = true;
76
+
77
+ try {
78
+ api.preload(JSON.parse(node.textContent || '{}'));
79
+ } catch {
80
+ /* not JSON: ignored */
81
+ }
82
+ };
83
+
84
+ const notify = (key, value) => {
85
+ for (const listener of listeners.get(key) ?? []) {
86
+ try {
87
+ listener(plain(value), key);
88
+ } catch (error) {
89
+ console.error('SF.Preferences listener failed', key, error);
90
+ }
91
+ }
92
+ };
93
+
94
+ const readCache = key => {
95
+ const store = storage();
96
+ if (!store) return null;
97
+
98
+ try {
99
+ const raw = store.getItem(PREFIX + key);
100
+ return raw === null ? null : JSON.parse(raw);
101
+ } catch {
102
+ return null;
103
+ }
104
+ };
105
+
106
+ const writeCache = (key, value) => {
107
+ const store = storage();
108
+ if (!store) return;
109
+
110
+ try {
111
+ if (value === null) store.removeItem(PREFIX + key);else store.setItem(PREFIX + key, JSON.stringify(value));
112
+ } catch {// A full or refused storage keeps the value for this page only.
113
+ }
114
+ };
115
+
116
+ const saveToHost = key => {
117
+ clearTimeout(timers.get(key));
118
+ timers.delete(key);
119
+ const adapter = host.adapter;
120
+ if (!adapter) return;
121
+ const value = host.values.has(key) ? host.values.get(key) : readCache(key);
122
+
123
+ try {
124
+ const answer = value === null ? adapter.remove?.(key) : adapter.save?.(key, plain(value));
125
+ Promise.resolve(answer).catch(error => console.warn('SF.Preferences: the host did not keep', key, error));
126
+ } catch (error) {
127
+ console.warn('SF.Preferences: the host did not keep', key, error);
128
+ }
129
+ };
130
+
131
+ const schedule = key => {
132
+ if (!host.adapter) return;
133
+ clearTimeout(timers.get(key));
134
+ timers.set(key, setTimeout(() => saveToHost(key), DELAY));
135
+ };
136
+
137
+ const api = {
138
+ // The value for a key: the host's when it gave one, else this browser's.
139
+ read(key) {
140
+ if (!validKey(key)) return null;
141
+ readInline();
142
+ if (host.values.has(key)) return plain(host.values.get(key));
143
+ return plain(readCache(key));
144
+ },
145
+
146
+ // Several keys from the most general to the most particular, merged field
147
+ // by field: `sf-table/*` (the person's tables) under `sf-table/<instance>`.
148
+ readMerged(...keys) {
149
+ const merged = {};
150
+ let found = false;
151
+
152
+ for (const key of keys.flat()) {
153
+ const value = api.read(key);
154
+
155
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
156
+ Object.assign(merged, value);
157
+ found = true;
158
+ }
159
+ }
160
+
161
+ return found ? merged : null;
162
+ },
163
+
164
+ write(key, value) {
165
+ if (!validKey(key)) return false;
166
+ const next = plain(value); // The value already kept is not written again: a component restoring
167
+ // what the host gave it (sf-resizer's value) sent it back to the host on
168
+ // every load (docs trial 5).
169
+
170
+ if (JSON.stringify(api.read(key)) === JSON.stringify(next)) return true;
171
+ if (host.values.has(key)) host.values.set(key, next);
172
+ writeCache(key, next);
173
+ schedule(key);
174
+ return true;
175
+ },
176
+
177
+ // «Сбросить вид»: gone from both.
178
+ remove(key) {
179
+ if (!validKey(key)) return false;
180
+ if (host.values.has(key)) host.values.set(key, null);
181
+ writeCache(key, null);
182
+ schedule(key);
183
+ notify(key, null);
184
+ return true;
185
+ },
186
+
187
+ subscribe(key, listener) {
188
+ if (!validKey(key) || typeof listener !== 'function') return () => {};
189
+ if (!listeners.has(key)) listeners.set(key, new Set());
190
+ listeners.get(key).add(listener);
191
+ return () => listeners.get(key)?.delete(listener);
192
+ },
193
+
194
+ // The host's values for this person. Each that differs from this
195
+ // browser's copy replaces it and is announced, so a component re-applies.
196
+ preload(values) {
197
+ if (!values || typeof values !== 'object') return;
198
+
199
+ for (const [key, value] of Object.entries(values)) {
200
+ if (!validKey(key)) continue;
201
+ const next = plain(value);
202
+ const before = api.read(key);
203
+ host.values.set(key, next);
204
+ writeCache(key, next);
205
+ if (!same(before, next)) notify(key, next);
206
+ }
207
+ },
208
+
209
+ // {save(key, value), remove(key), load?(keys) → Promise<{key: value}>}.
210
+ useHost(adapter) {
211
+ if (!adapter || typeof adapter.save !== 'function') throw new TypeError('A preferences host saves: {save(key, value)}');
212
+ host.adapter = adapter;
213
+ return api;
214
+ },
215
+
216
+ // Asks the host for keys it has not given yet (a component that appears
217
+ // after the page loaded).
218
+ async load(keys) {
219
+ const wanted = [].concat(keys).filter(key => validKey(key) && !host.values.has(key));
220
+ if (!wanted.length || typeof host.adapter?.load !== 'function') return;
221
+
222
+ try {
223
+ api.preload(await host.adapter.load(wanted));
224
+ } catch (error) {
225
+ console.warn('SF.Preferences: the host did not answer', wanted, error);
226
+ }
227
+ },
228
+
229
+ get hosted() {
230
+ return Boolean(host.adapter);
231
+ },
232
+
233
+ // Writes still waiting go now (a page that is leaving).
234
+ flush() {
235
+ for (const key of [...timers.keys()]) saveToHost(key);
236
+ }
237
+
238
+ }; // Another tab changed a preference.
239
+
240
+ globalThis.addEventListener?.('storage', event => {
241
+ if (!event.key?.startsWith(PREFIX) || event.storageArea !== storage()) return;
242
+ const key = event.key.slice(PREFIX.length);
243
+ if (host.values.has(key)) return;
244
+ let value = null;
245
+
246
+ try {
247
+ value = event.newValue === null ? null : JSON.parse(event.newValue);
248
+ } catch {
249
+ value = null;
250
+ }
251
+
252
+ notify(key, value);
253
+ });
254
+ globalThis.addEventListener?.('pagehide', () => api.flush());
255
+ readInline();
256
+ return Object.freeze(api);
257
+ }
258
+
259
+ if (typeof globalThis !== 'undefined') {
260
+ globalThis.SF = globalThis.SF || {};
261
+ if (!globalThis.SF.Preferences) globalThis.SF.Preferences = createPreferences();
262
+ }
263
+
264
+ const Preferences = globalThis.SF?.Preferences;
265
+ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (Preferences);
266
+
267
+
268
+ /***/ },
269
+
270
+ /***/ "17828be41e75"
271
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
272
+
273
+ __webpack_require__.r(__webpack_exports__);
274
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
275
+ /* harmony export */ answerActions: () => (/* binding */ answerActions),
276
+ /* harmony export */ outcomeOf: () => (/* binding */ outcomeOf),
277
+ /* harmony export */ settingSetters: () => (/* binding */ settingSetters)
278
+ /* harmony export */ });
279
+ // The actions a manifest declares, on a class that already has the methods
280
+ // (standard simai.smart-component 1.3.8 §4.4). elementFromManifest makes a
281
+ // method per action, but a component whose class predates its manifest keeps
282
+ // its own methods, which shadow the generated ones. This wraps those methods
283
+ // once per class so that, as the standard asks, each:
284
+ //
285
+ // - takes its `with` parameters as one object, as well as positionally;
286
+ // - answers an outcome: {outcome: applied | refused | conflict | unavailable
287
+ // | cancelled, reason?, message?}. A host port's answer ({answer, reason,
288
+ // message}, vocabulary §3.1) is read as the outcome it is; a method that
289
+ // answered nothing, or the element itself, answered applied.
290
+ //
291
+ // Helpers the class calls internally go through the same wrapper; nothing in
292
+ // the package reads their old return value (the element, or the port's
293
+ // answer under `answer`), and `answer` stays on the outcome for one that does.
294
+ const camel = name => String(name).replace(/-([a-z0-9])/gu, (_, letter) => letter.toUpperCase());
295
+
296
+ const isPlain = value => value !== null && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
297
+
298
+ function outcomeOf(result, element) {
299
+ if (result === undefined || result === null || result === true || result === element) return {
300
+ outcome: "applied"
301
+ };
302
+ if (result === false) return {
303
+ outcome: "refused",
304
+ reason: "refused"
305
+ };
306
+ if (typeof result === "object" && typeof result.outcome === "string") return result;
307
+
308
+ if (typeof result === "object" && typeof result.answer === "string") {
309
+ const {
310
+ answer,
311
+ reason,
312
+ message
313
+ } = result;
314
+ return { ...result,
315
+ outcome: answer,
316
+ ...(reason ? {
317
+ reason
318
+ } : {}),
319
+ ...(message ? {
320
+ message
321
+ } : {})
322
+ };
323
+ }
324
+
325
+ return {
326
+ outcome: "applied",
327
+ value: result
328
+ };
329
+ }
330
+ function answerActions(Class, manifest) {
331
+ const proto = Class.prototype;
332
+
333
+ for (const [name, spec] of Object.entries(manifest.actions || {})) {
334
+ const method = camel(name);
335
+ const run = proto[method];
336
+ if (typeof run !== "function" || run.sfAction) continue;
337
+ const names = Object.keys(spec?.with || {});
338
+
339
+ const wrapped = function manifestAction(...args) {
340
+ // One object naming only declared parameters is the object form.
341
+ if (args.length === 1 && isPlain(args[0])) {
342
+ const given = Object.keys(args[0]);
343
+ if (!names.length && !given.length) args = [];else if (names.length && given.length && given.every(key => names.includes(key) || names.map(camel).includes(key))) {
344
+ args = names.map(key => args[0][key] ?? args[0][camel(key)]);
345
+ }
346
+ }
347
+
348
+ const result = run.apply(this, args);
349
+ if (result && typeof result.then === "function") return result.then(value => outcomeOf(value, this));
350
+ return outcomeOf(result, this);
351
+ };
352
+
353
+ wrapped.sfAction = true;
354
+ Object.defineProperty(proto, method, {
355
+ configurable: true,
356
+ writable: true,
357
+ value: wrapped
358
+ });
359
+ }
360
+
361
+ return Class;
362
+ } // A setting the class reads through a getter of its own (an attribute read
363
+ // with its parsing) is writable as a property too, as §4 makes every setting:
364
+ // an assembly binds by property when the element has one (SF.Assembly's
365
+ // adaptElement), and a getter alone threw «has only a getter» at the first
366
+ // binding. The setter writes the attribute the getter reads.
367
+
368
+ function settingSetters(Class, manifest) {
369
+ const proto = Class.prototype;
370
+
371
+ for (const [name, spec] of Object.entries(manifest.settings || {})) {
372
+ if (["list", "object", "state"].includes(spec?.type)) continue;
373
+ const property = camel(name);
374
+ let owner = proto;
375
+ let found = null;
376
+
377
+ while (owner && owner !== globalThis.HTMLElement?.prototype && owner !== Object.prototype) {
378
+ found = Object.getOwnPropertyDescriptor(owner, property);
379
+ if (found) break;
380
+ owner = Object.getPrototypeOf(owner);
381
+ }
382
+
383
+ if (!found || !found.get || found.set) continue;
384
+ Object.defineProperty(owner, property, {
385
+ configurable: true,
386
+ get: found.get,
387
+
388
+ set(value) {
389
+ if (spec.type === "boolean") {
390
+ if (value === true || value === "" || value === "true") this.setAttribute(name, "");else if (value === false && spec.default === true) this.setAttribute(name, "false");else this.removeAttribute(name);
391
+ } else if (value === null || value === undefined) {
392
+ this.removeAttribute(name);
393
+ } else {
394
+ this.setAttribute(name, String(value));
395
+ }
396
+ }
397
+
398
+ });
399
+ }
400
+
401
+ return Class;
402
+ }
403
+
404
+ /***/ },
405
+
406
+ /***/ "36b027867bab"
407
+ (module, __webpack_exports__, __webpack_require__) {
408
+
409
+ __webpack_require__.a(module, async (__webpack_handle_async_dependencies__, __webpack_async_result__) => { try {
410
+ __webpack_require__.r(__webpack_exports__);
411
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
412
+ /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
413
+ /* harmony export */ });
414
+ /* harmony import */ var _js_first_paint_css__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("4d9330f13af8");
415
+ /* harmony import */ var _core_js_page_language__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("43090d1c8307");
416
+ /* harmony import */ var _core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__("5b7d7695b41b");
417
+ /* harmony import */ var _core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__("deb1a4d61565");
418
+ /* harmony import */ var _core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__("892a0da25239");
419
+ /* harmony import */ var _view_table_js_manifest_actions_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__("17828be41e75");
420
+ /* harmony import */ var _component_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__("7086146c00cc");
421
+ /* harmony import */ var _aliases_json__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__("80ad38cb6908");
422
+ /* harmony import */ var _lang_en_json__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__("ce0305e57fa5");
423
+ /* harmony import */ var _lang_ru_json__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__("3e9dffe84919");
424
+ /* harmony import */ var _core_js_preferences_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__("51250bf01ab0");
425
+ /* harmony import */ var _js_tree_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__("29f88c884ba1");
426
+ var __webpack_async_dependencies__ = __webpack_handle_async_dependencies__([_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__, _core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2__, _core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__, _core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4__]);
427
+ ([_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__, _core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2__, _core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__, _core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4__] = __webpack_async_dependencies__.then ? (await __webpack_async_dependencies__)() : __webpack_async_dependencies__);
428
+ /* harmony import */ var _core_js_page_language__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__);
429
+ /* harmony import */ var _core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2__);
430
+ /* harmony import */ var _core_js_smart_base__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__);
431
+ /* harmony import */ var _core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4__);
432
+ // A composition shell. The host owns access, storage and query semantics;
433
+ // this element owns the browser lifecycle and projects answers into its slots.
434
+ // sf-view-table, sf-pagination and sf-toast are not imported: a package carries no
435
+ // element layer of its own, and an element that needs another asks for it by
436
+ // tag (ui-source docs/developer/shared-runtime.md). The rule's relations bring
437
+ // them (cl-view-table, cl-pagination, cl-toast lazy), and the view asks the Loader
438
+ // for any that is not defined yet (_askForParts). Imported, each was a second
439
+ // copy of the element in data-view.min.js (431 KB min; stand 25).
440
+ // The view's own first-paint styles, its skeleton, are its sheet.
441
+
442
+
443
+ // The renamed tags (sf-view-table for sf-table, sf-view for sf-data-view until
444
+ // 2027-01-01): a lookup by name takes both through these, and the view defines
445
+ // its former tag beside its own (it is not a Smart base element).
446
+
447
+
448
+
449
+
450
+
451
+
452
+
453
+
454
+
455
+
456
+ const LISTENED = Object.freeze(["sf-view-table-search-change", "sf-view-table-filter-change", "sf-view-table-sort-change", "sf-view-table-save", "sf-view-table-apply", "sf-view-table-refresh", "sf-view-table-select", "sf-view-table-column-settings-reset", "sf-view-table-expand", "sf-view-table-load-more", "sf-view-table-reference-open", "sf-view-table-cell-set", "sf-view-table-move", "sf-pagination-change", "sf-pagination-size-change", "sf-pagination-apply", "sf-pagination-load-more", "sf-pagination-selection-clear", "sf-pagination-count", "sf-pagination-action-change", "sf-pagination-action-for-all-change", "sf-view-saved-view-intent"]); // The table's and the page row's events by their names since standard 1.3.8
457
+ // (vocabulary 1.1.5): the old names they still send until 2026-12-01 are not
458
+ // listened to, so each act is taken once.
459
+
460
+ const EVENTS = Object.freeze({
461
+ "sf-view-table-query": "query.change",
462
+ "sf-view-table-template-select": "view.open",
463
+ "sf-view-table-template-save": "view.save",
464
+ "sf-view-table-template-delete": "view.delete",
465
+ "sf-view-table-create": "view.create_record"
466
+ });
467
+ let instanceNumber = 0; // Every port version this composite accepts, in one place. The list used to be
468
+ // written inline in the guard, and two versions were published — 1.3.0 for a
469
+ // host that does not count, 1.4.0 for personal pinning — while the guard still
470
+ // named 1.2.0 as the newest it would take. A host moving to either would have
471
+ // been refused outright by the component that asked it to move. The contract
472
+ // test beside this file now compares the list with the manifest.
473
+ // The port speaks snake_case and the table reads camelCase, and the translation
474
+ // is this view's job: a column that named its property_type reached the table
475
+ // under a name nothing read, so the cell drew the stored value. Both names are
476
+ // kept -- the port's, because that is what the host sent and what a consumer
477
+ // reading the DOM expects, and the component's, because that is what the shared
478
+ // renderer set asks for.
479
+
480
+ const COLUMN_PRESENTATION = {
481
+ property_type: "propertyType",
482
+ min_width: "minWidth"
483
+ }; // The sentence a host answer carries for a person (port answer `message`):
484
+ // a non-empty string, trimmed; anything else is no message.
485
+ // A chrome part the host turned off on its own table (selectable="false",
486
+ // settings="false") stays off. The view writes these attributes on every
487
+ // projection, so what the table carried before the view first touched it is
488
+ // kept aside, as data-sf-host-row-actions is, and read from there.
489
+
490
+ function hostWroteOff(table, name) {
491
+ if (!table) return false;
492
+ const marker = `data-sf-host-${name}`;
493
+ if (!table.hasAttribute?.(marker)) table.setAttribute?.(marker, table.getAttribute?.(name) ?? "");
494
+ return table.getAttribute?.(marker) === "false";
495
+ }
496
+
497
+ function hostMessage(answer) {
498
+ return typeof answer?.message === "string" ? answer.message.trim() : "";
499
+ } // Whether a refused list was refused for access rather than for the query
500
+ // (Larena acceptance, 2026-10-22: a search longer than the host's limit,
501
+ // refused as query_invalid, drew «Нет доступа» and took the toolbar away, so
502
+ // the search could not be shortened without reloading). The port's reason is
503
+ // free text a host may extend (simai.dataview-port 1.12.0 answer.reason) and the
504
+ // contract names one access reason, `forbidden`; it has no class for the rest.
505
+ // Access is read from the code: `forbidden`, `access_denied`, `scope_denied`,
506
+ // and a host's own prefixed form of the last two (`storage_page_scope_denied`,
507
+ // `registered_list_access_denied`). A refusal without a reason stays a refusal
508
+ // of access, as every refused list was before. Any other reason is about the
509
+ // query: the person keeps the toolbar and the query to correct it.
510
+
511
+
512
+ const ACCESS_REFUSALS = new Set(["forbidden", "access_denied", "scope_denied"]);
513
+
514
+ function isAccessRefusal(reason) {
515
+ if (reason == null || reason === "") return true; // Larena writes the codes with underscores; ui-admin's reference host with
516
+ // hyphens (access-denied, stand 24 U2): both are the same refusal.
517
+
518
+ const code = String(reason).replace(/-/gu, "_");
519
+ return ACCESS_REFUSALS.has(code) || code.endsWith("_access_denied") || code.endsWith("_scope_denied");
520
+ }
521
+
522
+ function projectColumn(column) {
523
+ const projected = copy(column);
524
+
525
+ for (const [sent, read] of Object.entries(COLUMN_PRESENTATION)) {
526
+ if (projected[sent] !== undefined && projected[read] === undefined) {
527
+ projected[read] = projected[sent];
528
+ }
529
+ }
530
+
531
+ return projected;
532
+ }
533
+
534
+ const SUPPORTED_PORT_VERSIONS = ["1.0.0", "1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0", "1.6.0", "1.7.0", "1.8.0", "1.9.0", "1.10.0", "1.11.0", "1.12.0", "1.13.0"]; // Creating a record and saving personal column settings became capabilities in
535
+ // port 1.10.0. A host that declares an earlier version was offered both without
536
+ // announcing them and keeps that; a host on 1.10.0 is offered them only when it
537
+ // announces them, so a page is never given a control nobody would answer.
538
+
539
+ const BASELINE_UNTIL_1_10 = new Set(["create-record", "personal-column-settings"]);
540
+
541
+ const portAtLeast = (version, minimum) => {
542
+ const parts = value => String(value ?? "").split(".").map(part => Number.parseInt(part, 10) || 0);
543
+
544
+ const [a, b] = [parts(version), parts(minimum)];
545
+
546
+ for (let index = 0; index < 3; index += 1) {
547
+ if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) > (b[index] ?? 0);
548
+ }
549
+
550
+ return true;
551
+ }; // The words are the language files' (lang/<locale>.json, standard 1.3.8 §11),
552
+ // read under the camelCase keys a product registers more languages by.
553
+
554
+
555
+ const camelWords = (words = {}) => Object.fromEntries(Object.entries(words).map(([key, word]) => [key.replace(/-([a-z0-9])/gu, (_, letter) => letter.toUpperCase()), word]));
556
+
557
+ const DATA_VIEW_TEXT = {
558
+ en: camelWords(_lang_en_json__WEBPACK_IMPORTED_MODULE_8__.runtime),
559
+ ru: camelWords(_lang_ru_json__WEBPACK_IMPORTED_MODULE_9__.runtime)
560
+ }; // The parts this element draws itself and a consumer can leave out. `status` is
561
+ // absent on purpose and cannot be switched off: a list that could not say a load
562
+ // failed would be a list that looks empty when it is broken, and an embedded one
563
+ // with a row cap and no chrome is exactly where someone would be tempted to
564
+ // remove it. `bulk` and `notice` are host slots that render nothing when empty,
565
+ // so switching them would be theatre.
566
+ // `select` and `settings` are the table's two chrome columns rather than parts of
567
+ // this element, and they are named here because they are furniture of the same
568
+ // kind: a checkbox column exists to feed a bulk band and a gear column exists to
569
+ // reach column settings, and an embedded list has neither. Named explicitly
570
+ // instead of inferred from the toolbar, so a consumer who does want a selection
571
+ // in an embedded list can ask for it -- `chrome="select"` -- rather than having to
572
+ // turn the whole toolbar back on to get a checkbox.
573
+ // Words the view passes on to the table and the pagination it draws.
574
+
575
+ const TABLE_LABELS = ["column-search-placeholder", "column-search-empty-text", "column-move-label", "filter-apply-text"];
576
+ const PAGINATION_LABELS = ["show-more-text", "selected-label", "total-label", "pages-label", "first-label", "previous-label", "page-label", "next-label", "last-label", "page-field-label", "action-choose-label", "actions-label", "actions-region-label", "clear-selection-label", "page-size-label"];
577
+ const LABEL_ATTRIBUTES = [...TABLE_LABELS, ...PAGINATION_LABELS, "pagination-aria-label"];
578
+ const CHROME_PARTS = ["toolbar", "pagination", "select", "settings"]; // Writes that cannot have changed a row, so the view does not re-query after
579
+ // them. Opening a record changes nothing either; the rest of the settings layer
580
+ // is the person's own view of the same rows.
581
+
582
+ const ANSWER_ONLY = new Set(["node.targets"]);
583
+ const TREE_INTENTS = new Set(["tree.expand", "node.targets", "node.move"]);
584
+ const NO_REFRESH_AFTER = new Set(["record.open", "tree.expand", "node.targets", "settings.save_personal", "settings.save_shared"]);
585
+
586
+ const copy = value => value == null ? value : JSON.parse(JSON.stringify(value)); // Whether a filter value holds anything: an empty string, list or object is
587
+ // no filter.
588
+ // SF.Preferences: the view's sort and page size are the person's (owner,
589
+ // 2026-10-14), kept under the same key as the table's arrangement.
590
+
591
+
592
+ const preferences = () => globalThis.SF?.Preferences || _core_js_preferences_js__WEBPACK_IMPORTED_MODULE_10__["default"]; // The stored key keeps the former tag's spelling: it names what a person has
593
+ // already saved (SF.Preferences, a host's store), not the element.
594
+
595
+
596
+ const viewPreferenceKey = key => `sf-table/${String(key).replace(/[^\w.:-]+/gu, "-")}`;
597
+
598
+ function hasValue(value) {
599
+ if (value === null || value === undefined || value === "" || value === false) return false;
600
+ if (Array.isArray(value)) return value.some(hasValue);
601
+ if (typeof value === "object") return Object.values(value).some(hasValue);
602
+ return true;
603
+ } // sf-view from its component.json (standard simai.smart-component 1.3.8
604
+ // §3, §4, §8, §11): a leaf over plain HTMLElement (it draws into its own
605
+ // shadow root), its settings and values made by core/js/element-manifest.js,
606
+ // its actions answering outcomes (../table/js/manifest-actions.js), its events by the
607
+ // vocabulary's verbs with the old names as dated aliases (aliases.json).
608
+
609
+
610
+ class PlainElement extends HTMLElement {
611
+ connectedCallback() {}
612
+
613
+ }
614
+
615
+ const DataViewElement = (0,_core_js_element_manifest__WEBPACK_IMPORTED_MODULE_2__.elementFromManifest)(_component_json__WEBPACK_IMPORTED_MODULE_6__, {
616
+ base: PlainElement,
617
+ lang: {
618
+ en: _lang_en_json__WEBPACK_IMPORTED_MODULE_8__,
619
+ ru: _lang_ru_json__WEBPACK_IMPORTED_MODULE_9__
620
+ },
621
+ source: "ru",
622
+ aliases: _aliases_json__WEBPACK_IMPORTED_MODULE_7__
623
+ }); // The first paint's rows when nothing says how many the page asks for (sf-view-table's
624
+ // page, its pagination-page-size default), how many fields a record of the
625
+ // compact list shows under its title (sf-view-table list-fields).
626
+
627
+ const FIRST_PAINT_ROWS = 10;
628
+ const FIRST_PAINT_LIST_FIELDS = 3; // The vocabulary's data state (§3) for the view's own words for it.
629
+
630
+ const DATA_STATES = {
631
+ loading: "loading",
632
+ populated: "ready",
633
+ empty: "empty",
634
+ error: "error",
635
+ "no-access": "refused",
636
+ unavailable: "unavailable"
637
+ }; // A value as text with its keys in order, so two answers that say the same
638
+ // thing compare equal whatever order a server wrote the keys in.
639
+
640
+ const canonical = value => JSON.stringify(value, (key, item) => item && typeof item === "object" && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map(name => [name, item[name]])) : item); // What a port offers, as the projection drew it.
641
+
642
+
643
+ const capabilitiesKey = port => canonical([...(port?.capabilities ?? [])].map(String).sort()); // Whether the port's answer is the one the page printed: the same revision
644
+ // when both name one, else the same rows, count and columns.
645
+
646
+
647
+ function sameAnswer(printed, data) {
648
+ if (printed.revision !== undefined && printed.revision !== null && data.revision !== undefined && data.revision !== null) {
649
+ return String(printed.revision) === String(data.revision);
650
+ }
651
+
652
+ const shape = answer => canonical({
653
+ records: answer.records ?? null,
654
+ total: answer.total ?? null,
655
+ has_next: answer.has_next ?? null,
656
+ columns: answer.columns ?? null,
657
+ tree: answer.tree ?? null
658
+ });
659
+
660
+ return shape(printed) === shape(data);
661
+ }
662
+
663
+ class SfDataView extends DataViewElement {
664
+ constructor() {
665
+ super();
666
+ this.instanceId = `sf-view-${++instanceNumber}`;
667
+ this.query = {
668
+ filters: {},
669
+ search: "",
670
+ sort: [],
671
+ page: 1,
672
+ per_page: 25
673
+ };
674
+ this.records = []; // The tree, when the host answers in levels (port 1.8.0): mode is
675
+ // tree, flat or flattened as the last answer said, null before one.
676
+
677
+ this.treeMode = null;
678
+ this.tree = (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.createTree)();
679
+ this._treeAppend = new Set();
680
+ this.treeSequence = 0; // The host's labels and groups for its own action ids (row_actions).
681
+
682
+ this.rowActionCatalog = [];
683
+ this.continuing = false;
684
+ this.appended = null;
685
+ this.columns = [];
686
+ this.total = 0; // Whether the host counts. It is answered by the answers themselves: a
687
+ // total means there is a number, no total means there is a link. There
688
+ // is no mode attribute, because a view whose host stops counting should
689
+ // not need a host to be reconfigured as well.
690
+
691
+ this.countKnown = true; // A host may answer an estimate (`total_exact: false`): the number is
692
+ // shown, but as «~N», and nothing is applied under the filter on it.
693
+
694
+ this.totalExact = true;
695
+ this.hasNext = false;
696
+ this.countedQuery = null;
697
+ this.savedViews = [];
698
+ this.columnSettings = null;
699
+ this.sequence = 0;
700
+ this.epoch = 0;
701
+ this.pending = null;
702
+ this.hostPort = null; // Whether a projection has arrived. Until one has, the counts the host
703
+ // wrote in the markup are left alone.
704
+
705
+ this._answered = false;
706
+
707
+ this._bound = event => this._onEvent(event);
708
+
709
+ this._seenEvents = new WeakSet();
710
+ this.attachShadow({
711
+ mode: "open"
712
+ });
713
+ this.shadowRoot.innerHTML = `
714
+ <style>
715
+ :host { display: block; }
716
+ [part="layout"] { display: grid; gap: var(--sf-data-view-gap, var(--sf-space-3)); }
717
+ [part="toolbar"] { display: flex; flex-wrap: wrap; gap: var(--sf-data-view-toolbar-gap, var(--sf-space-2)); }
718
+ /* Hidden means hidden: the toolbar's own display beat the hidden
719
+ attribute, and a related list in a record panel (chrome="")
720
+ showed the bare fallback search above its rows (Larena,
721
+ 2026-10-14). */
722
+ [hidden] { display: none !important; }
723
+ /* Files from the computer over a view that takes them (accept-files):
724
+ a dashed zone over the list says where they go. */
725
+ [part="drop"] {
726
+ position: absolute; inset: 0; z-index: 20;
727
+ display: grid; place-items: center;
728
+ border: var(--sf-focus--width, 2px) dashed var(--sf-data-view-drop-border-color, var(--sf-primary));
729
+ border-radius: var(--sf-radius--surface);
730
+ background: var(--sf-data-view-drop-background-color, color-mix(in srgb, var(--sf-primary-container) 80%, transparent));
731
+ color: var(--sf-data-view-drop-color, var(--sf-on-primary-container));
732
+ font: var(--sf-font-weight-medium) var(--sf-text-size-1) / var(--sf-text-height-1) var(--sf-text--family);
733
+ pointer-events: none;
734
+ }
735
+ /* What the view is doing is announced, not laid out. It used to be
736
+ a line above the table, which said "Нет данных" over a table
737
+ whose own body says the same, and pushed everything down by its
738
+ height every time a search found nothing. */
739
+ [part="status"] {
740
+ position: absolute;
741
+ inline-size: var(--sf-px);
742
+ block-size: var(--sf-px);
743
+ margin: calc(var(--sf-px) * -1);
744
+ padding: 0;
745
+ overflow: hidden;
746
+ clip-path: inset(50%);
747
+ white-space: nowrap;
748
+ border: 0;
749
+ }
750
+ /* A notice is a message about an action. It used to float over the
751
+ top of the card, which moved nothing but covered the search, the
752
+ create button and the filters -- the consumer was right that this
753
+ is worse than a shifted table. It stands in the corner of the
754
+ window now, over the page and over none of the controls, and it
755
+ goes away by itself. Slotted from the light DOM because the
756
+ Framework's styles -- sf-toast's among them -- do not cross a
757
+ shadow boundary. */
758
+ slot[name="notice"]::slotted(*) {
759
+ position: fixed;
760
+ inset-block-end: var(--sf-space-3);
761
+ inset-inline-end: var(--sf-space-3);
762
+ max-inline-size: var(--sf-g0);
763
+ /* One above the menus: they share the top step, and a refusal
764
+ answering a menu choice opened under the menu that asked. */
765
+ z-index: calc(var(--sf-z-index-9, 90) + 1);
766
+ }
767
+ /* The band of bulk controls is the last strip of the card and holds
768
+ the bottom of the window while the card runs past it. It is
769
+ slotted rather than built in here: the Framework's own styles do
770
+ not cross a shadow boundary, so the band lives in the light DOM
771
+ where they reach it, and this layout is what contains it. */
772
+ slot[name="bulk"]::slotted(*) {
773
+ position: sticky;
774
+ inset-block-end: 0;
775
+ z-index: var(--sf-z-index-3, 30);
776
+ }
777
+ /* The first paint (standard 1.3.8 §5.4): until the host's first
778
+ answer the view is its skeleton, slotted from the light DOM
779
+ where a server printed it (static/<template>/<example>.html)
780
+ or where the view drew it, and the layout stands back. */
781
+ :host([data-sf-awaiting]) [part="layout"] { display: none; }
782
+ :host(:not([data-sf-awaiting])) slot[name="first-paint"] { display: none; }
783
+ </style>
784
+ <slot name="first-paint"></slot>
785
+ <section part="layout" style="position: relative">
786
+ <div part="toolbar"><slot name="toolbar">
787
+ <label>Поиск <input type="search" data-search></label>
788
+ <!-- Saved views are offered only when the host says it keeps them.
789
+ They were offered always, and a host that claims neither
790
+ saved-views nor shared-saved-views answers every one of these
791
+ with capability-not-provided -- so «Сохранить вид» could do
792
+ nothing but refuse, which is not a control, it is a trap. The
793
+ owner met them on a page whose host claims only row-actions
794
+ (owner, 2026-10-09). -->
795
+ <span data-saved-views hidden>
796
+ <label>Вид <select data-view><option value="">Текущий</option></select></label>
797
+ <label>Название вида <input type="text" data-view-name></label>
798
+ <button type="button" data-save>Сохранить вид</button>
799
+ <button type="button" data-delete>Удалить вид</button>
800
+ </span>
801
+ </slot></div>
802
+ <div part="status" role="status" aria-live="polite" hidden></div>
803
+ <div part="drop" hidden></div>
804
+ <slot name="content"><sf-view-table toolbar="false"></sf-view-table></slot>
805
+ <slot name="show-all"></slot>
806
+ <slot name="pagination"><sf-pagination top="false" bottom="false"></sf-pagination></slot>
807
+ <slot name="bulk"></slot>
808
+ <slot name="notice"></slot>
809
+ <slot name="dialog"></slot>
810
+ </section>`;
811
+ } // ---------- the manifest's values and settings (§4) ----------
812
+
813
+
814
+ get total() {
815
+ return this._total ?? 0;
816
+ }
817
+
818
+ set total(value) {
819
+ this._total = value;
820
+ } // The vocabulary's data state of the list (§3): ready for populated,
821
+ // refused for no access.
822
+
823
+
824
+ get state() {
825
+ if (this._queryRefused) return "refused";
826
+ return DATA_STATES[this._dataState] ?? "idle";
827
+ } // The row the page has open, marked in the table (aria-current).
828
+
829
+
830
+ get current() {
831
+ return this._current ?? "";
832
+ }
833
+
834
+ set current(id) {
835
+ this._current = id === null || id === undefined ? "" : String(id);
836
+ if (this.table) this.table.current = this._current;
837
+ }
838
+
839
+ get selected() {
840
+ return this._current || undefined;
841
+ } // The actions offered for the marked rows, [{id, label}] (component.json
842
+ // settings.bulk-actions). A page writes them on its page row's markup
843
+ // (actions="publish:Опубликовать|…"); an assembly that places the bare view
844
+ // gives them here, their words from its language file (§11.3), and the view
845
+ // hands them to its page row.
846
+
847
+
848
+ get bulkActions() {
849
+ return this._bulkActions ?? [];
850
+ }
851
+
852
+ set bulkActions(list) {
853
+ this._bulkActions = Array.isArray(list) ? list.filter(item => item && (item.id ?? item.value)) : [];
854
+
855
+ this._applyBulkActions();
856
+ }
857
+
858
+ _applyBulkActions() {
859
+ const pagination = this.pagination;
860
+ if (!pagination || !this._bulkActions?.length) return;
861
+
862
+ const clean = text => String(text ?? "").replace(/[|:]/gu, " ").trim();
863
+
864
+ const markup = this._bulkActions.map(item => `${clean(item.id ?? item.value)}:${clean(item.label ?? item.text ?? item.id ?? item.value)}`).join("|");
865
+
866
+ if (pagination.getAttribute("actions") !== markup) pagination.setAttribute("actions", markup);
867
+ }
868
+
869
+ get selection() {
870
+ return this.table?.selection ?? [];
871
+ }
872
+
873
+ get table() {
874
+ const slot = this.shadowRoot.querySelector('slot[name="content"]');
875
+ return slot.assignedElements().find(element => (0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.isTag)(element, "sf-view-table")) ?? slot.querySelector((0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view-table"));
876
+ } // The band of bulk controls, kept in the light DOM so the Framework styles
877
+ // reach it, and assigned to the layout's own slot so the card contains it.
878
+
879
+
880
+ get bulkBand() {
881
+ if (this._bulkBand?.isConnected) return this._bulkBand;
882
+ const existing = this.querySelector(':scope > [slot="bulk"]');
883
+ this._bulkBand = existing ?? document.createElement("div");
884
+
885
+ this._bulkBand.setAttribute("slot", "bulk");
886
+
887
+ this._bulkBand.classList.add("sf-pagination-actions", "flex", "items-cross-center");
888
+
889
+ this._bulkBand.setAttribute("role", "region");
890
+
891
+ if (!this._bulkBand.isConnected) this.append(this._bulkBand);
892
+ return this._bulkBand;
893
+ } // The show-all row, in the light DOM for the same reason the bulk band and the
894
+ // notice are: the Framework's stylesheets do not cross a shadow boundary, so a
895
+ // control drawn inside the shadow comes out as a native grey button. A consumer
896
+ // reported exactly that beside «5 of 61», and it is the same trap that had an
897
+ // embedded table showing its icon ligatures as text.
898
+
899
+
900
+ get showAllBand() {
901
+ if (this._showAll?.isConnected) return this._showAll;
902
+ const existing = this.querySelector(':scope > [slot="show-all"]');
903
+ this._showAll = existing ?? document.createElement("div");
904
+
905
+ this._showAll.setAttribute("slot", "show-all");
906
+
907
+ this._showAll.classList.add("flex", "items-cross-center", "gap-1/3"); // The click is listened for on the band rather than on this element. A
908
+ // listener on the element would see every click inside it, and the handler
909
+ // below branches on [data-save] and [data-delete] as well -- a row that
910
+ // happened to carry one of those would have reached the wrong branch.
911
+
912
+
913
+ if (this._showAll.getAttribute("data-sf-bound") !== "1") {
914
+ this._showAll.setAttribute("data-sf-bound", "1");
915
+
916
+ this._showAll.addEventListener("click", this._bound);
917
+ }
918
+
919
+ if (!this._showAll.isConnected) this.append(this._showAll);
920
+ return this._showAll;
921
+ } // The notice: one alert at a time, in the light DOM so the Framework's own
922
+ // styles reach it, assigned to the layout's slot so the card holds it.
923
+
924
+
925
+ get noticeBand() {
926
+ if (this._notice?.isConnected) return this._notice;
927
+ const existing = this.querySelector(':scope > [slot="notice"]');
928
+ this._notice = existing ?? document.createElement("div");
929
+
930
+ this._notice.setAttribute("slot", "notice");
931
+
932
+ if (!this._notice.isConnected) this.append(this._notice);
933
+ return this._notice;
934
+ } // What a reason code says to a person. A host that sends its own `message`
935
+ // is believed; a reason this component knows is translated; anything else
936
+ // is a plain sentence, with the code kept in the title for the people who
937
+ // read codes. A code is never the message: the consumer saw
938
+ // `minimal_cms_action_rejected` where a sentence belonged.
939
+
940
+
941
+ _noticeText(answer = {}) {
942
+ const message = hostMessage(answer);
943
+ if (message) return message; // The host's own sentence wins; these are what to say when it sent
944
+ // none. They follow the page's language like every other word.
945
+
946
+ const keys = {
947
+ "action-not-allowed": "noticeActionNotAllowed",
948
+ "capability-not-provided": "noticeCapabilityNotProvided",
949
+ "port-not-connected": "noticePortNotConnected",
950
+ "saved-view-levels-require-port-1.1.0": "noticeSavedViewLevels",
951
+ // The rest of the reasons simai.dataview-port 1.12.0 lists: each
952
+ // has its own words rather than the answer's general one.
953
+ "confirm_required": "noticeConfirmRequired",
954
+ "not-confirmed": "noticeNotConfirmed",
955
+ "no-base-revision": "noticeNoBaseRevision",
956
+ "record_invalid": "noticeRecordInvalid",
957
+ "forbidden": "noticeForbidden",
958
+ "stale-revision": "noticeStaleRevision"
959
+ };
960
+ if (keys[answer.reason]) return (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, keys[answer.reason], this);
961
+ const byAnswer = {
962
+ conflict: "noticeConflict",
963
+ refused: "noticeRefused",
964
+ unavailable: "noticeUnavailable"
965
+ }[answer.answer];
966
+ return (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, byAnswer ?? "noticeFailed", this);
967
+ } // Shown over the card, not in it. `role` follows what happened: an error is
968
+ // an alert, a refusal is a message.
969
+
970
+
971
+ notify(text, {
972
+ kind = "error",
973
+ code = ""
974
+ } = {}) {
975
+ // The page's one notification area (SF.notify, Framework
976
+ // ui-82fc0ba882ba): the view's messages go where every other message
977
+ // goes, and a notification centre records them from sf-notification
978
+ // (owner, 2026-10-13). A clear closes the view's last message there.
979
+ // A Framework without SF.notify keeps the band over the card.
980
+ if (typeof globalThis.SF?.notify === "function") {
981
+ if (!text) {
982
+ this._notifyHandle?.close?.();
983
+ this._notifyHandle = null;
984
+ return this;
985
+ }
986
+
987
+ this._notifyHandle = globalThis.SF.notify({
988
+ status: kind === "error" ? "error" : "warning",
989
+ title: text,
990
+ // Long enough to read a sentence, as the band's was.
991
+ duration: 7000,
992
+ source: code ? `sf-view:${code}` : "sf-view",
993
+ key: code ? `sf-view:${code}` : undefined
994
+ });
995
+ return this;
996
+ }
997
+
998
+ const band = this.noticeBand;
999
+ band.replaceChildren();
1000
+
1001
+ if (!text) {
1002
+ band.remove();
1003
+ this._notice = null;
1004
+ return this;
1005
+ }
1006
+
1007
+ this._askForParts(["sf-toast"]);
1008
+
1009
+ const toast = document.createElement("sf-toast");
1010
+ toast.setAttribute("status", kind === "error" ? "error" : "warning");
1011
+ toast.setAttribute("closable", "true");
1012
+ toast.setAttribute("close-label", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "closeNoticeLabel", this));
1013
+ toast.setAttribute("role", kind === "error" ? "alert" : "status");
1014
+ toast.setAttribute("heading", text); // Long enough to read a sentence, and it can be closed at once. A
1015
+ // message that stays until it is dismissed ends up covering something,
1016
+ // which is how the overlay before it went wrong.
1017
+
1018
+ toast.setAttribute("duration", "7000"); // The code is for the people who read codes, and they read it in the
1019
+ // inspector: shown as a second line it was a raw identifier under a
1020
+ // sentence, which the owner read as noise (2026-10-12).
1021
+
1022
+ if (code) toast.setAttribute("data-sf-code", code); // Closed, not asked to close: sf-toast-close is the cancelable request now.
1023
+
1024
+ toast.addEventListener("sf-toast-closed", () => this.notify(""));
1025
+ band.append(toast);
1026
+ return this;
1027
+ }
1028
+
1029
+ get pagination() {
1030
+ const slot = this.shadowRoot.querySelector('slot[name="pagination"]');
1031
+ return slot.assignedElements().find(element => element.matches("sf-pagination")) ?? slot.querySelector("sf-pagination");
1032
+ } // Files from the computer dropped on the view (accept-files, owner
1033
+ // 2026-10-15, the file manager) are the host's to upload: the view says
1034
+ // so with sf-data-view-files-drop {files}. They do not go through the
1035
+ // port, whose payloads are JSON.
1036
+
1037
+
1038
+ _bindFileDrop() {
1039
+ if (this._fileDropBound) return;
1040
+ this._fileDropBound = true;
1041
+
1042
+ const zone = () => this.shadowRoot.querySelector('[part="drop"]');
1043
+
1044
+ const carriesFiles = event => this.hasAttribute("accept-files") && [...(event.dataTransfer?.types ?? [])].includes("Files");
1045
+
1046
+ const show = on => {
1047
+ const node = zone();
1048
+ if (!node) return;
1049
+ node.textContent = (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "dropFiles", this);
1050
+ node.hidden = !on;
1051
+ };
1052
+
1053
+ this.addEventListener("dragover", event => {
1054
+ if (!carriesFiles(event)) return;
1055
+ event.preventDefault();
1056
+ event.dataTransfer.dropEffect = "copy";
1057
+ show(true);
1058
+ });
1059
+ this.addEventListener("dragleave", event => {
1060
+ if (!this.contains(event.relatedTarget) && !this.shadowRoot.contains(event.relatedTarget)) show(false);
1061
+ });
1062
+ this.addEventListener("drop", event => {
1063
+ if (!carriesFiles(event)) return;
1064
+ event.preventDefault();
1065
+ show(false);
1066
+ const files = [...event.dataTransfer.files];
1067
+ if (files.length) this.dispatchEvent(new CustomEvent("sf-view-files-drop", {
1068
+ bubbles: true,
1069
+ composed: true,
1070
+ detail: {
1071
+ files
1072
+ }
1073
+ }));
1074
+ });
1075
+ } // A view placed with no table and no page row of the page's own (an
1076
+ // assembly places the bare tag) makes them as its light children, slotted,
1077
+ // where the page's stylesheets reach them: the fallbacks inside the shadow
1078
+ // root are drawn without the table's sheet. The documented markup gives
1079
+ // them, and then nothing is made.
1080
+
1081
+
1082
+ _ownParts() {
1083
+ if (!this.querySelector(`:scope > ${(0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view-table")}[slot="content"], :scope > [slot="content"]`)) {
1084
+ const table = document.createElement("sf-view-table");
1085
+ table.slot = "content";
1086
+ table.setAttribute("aria-label", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "recordsLabel", this));
1087
+ table.toggleAttribute("data-sf-view-made", true);
1088
+ this.append(table);
1089
+ }
1090
+
1091
+ if (!this.querySelector(':scope > [slot="pagination"]')) {
1092
+ const pagination = document.createElement("sf-pagination");
1093
+ pagination.slot = "pagination";
1094
+ pagination.setAttribute("top", "false");
1095
+ pagination.setAttribute("bottom", "false");
1096
+ pagination.toggleAttribute("data-sf-view-made", true);
1097
+ this.append(pagination);
1098
+ }
1099
+
1100
+ this._applyBulkActions();
1101
+
1102
+ this._applyForAll();
1103
+ }
1104
+
1105
+ connectedCallback() {
1106
+ this._readPrintedPageSize();
1107
+
1108
+ super.connectedCallback();
1109
+ if (this.getAttribute("data-sf-instance")) this._ownParts();
1110
+
1111
+ this._askForParts(["sf-view-table", "sf-pagination"]); // Dated aliases (§16.20), installed once per element as the Smart base does.
1112
+
1113
+
1114
+ if (!this.__sfAliases) this.__sfAliases = (0,_core_js_aliases_js__WEBPACK_IMPORTED_MODULE_4__.installAliases)(this, _aliases_json__WEBPACK_IMPORTED_MODULE_7__, {
1115
+ tag: this.localName,
1116
+ adapters: {}
1117
+ });
1118
+
1119
+ this._bindFileDrop();
1120
+
1121
+ this.epoch += 1; // The key reaches the table before its first paint.
1122
+
1123
+ this._applyPreferences();
1124
+
1125
+ this._controller = new AbortController();
1126
+ const events = [...Object.keys(EVENTS), ...LISTENED];
1127
+
1128
+ for (const name of events) this.shadowRoot.addEventListener(name, this._bound);
1129
+
1130
+ for (const name of events) this.addEventListener(name, this._bound);
1131
+
1132
+ this.shadowRoot.querySelector("[data-search]").addEventListener("change", this._bound);
1133
+ this.shadowRoot.querySelector("[data-view]").addEventListener("change", this._bound);
1134
+ this.shadowRoot.querySelector("[data-save]").addEventListener("click", this._bound);
1135
+ this.shadowRoot.querySelector("[data-delete]").addEventListener("click", this._bound);
1136
+
1137
+ this._readDeclaredPort();
1138
+
1139
+ this._seedPageSize();
1140
+
1141
+ this._seedPrintedQuery();
1142
+
1143
+ this._renderProjection();
1144
+
1145
+ this._firstPaintReady = true;
1146
+
1147
+ this._drawFirstAnswer();
1148
+
1149
+ this._startFirstPaint();
1150
+
1151
+ if (this.hostPort) this.refresh("connected");else this._requestHostPort();
1152
+ } // ---------- the first paint (standard 1.3.8 §5.4) ----------
1153
+ // The rows the skeleton holds: the page the query asks for (a kept page
1154
+ // size, else the page size the page printed on its page row), no more than
1155
+ // the cap of an embedded list; else the cap; else a page of the table's
1156
+ // (sf-view-table pagination-page-size, 10). A skeleton of 10 rows over a page
1157
+ // of 20 grows the list when the rows come (stand 25 D1).
1158
+
1159
+
1160
+ get firstPaintRows() {
1161
+ const asked = this._declaredPageSize();
1162
+
1163
+ const cap = this.rowCap;
1164
+ if (asked && cap) return Math.min(asked, cap);
1165
+ return asked || cap || FIRST_PAINT_ROWS;
1166
+ } // The page size somebody declared: the person's kept one, a first answer's
1167
+ // echo, or the page row's page-size as the page printed it. Zero when
1168
+ // nobody did -- the query's own 25 is the view's default, not a request.
1169
+
1170
+
1171
+ _declaredPageSize() {
1172
+ if (this._pageSizeDeclared) return Number(this.query.per_page) || 0;
1173
+ return this._printedPageSize || 0;
1174
+ } // The page size the page printed on its page row, read once, before
1175
+ // anything of the view's writes its own page size there (the page row's
1176
+ // state, from the first projection on).
1177
+
1178
+
1179
+ _readPrintedPageSize() {
1180
+ if (this._printedPageSize !== undefined) return;
1181
+ const row = this.querySelector?.(':scope > sf-pagination[slot="pagination"]');
1182
+ if (!row) return;
1183
+ const printed = Number.parseInt(row.getAttribute("page-size") ?? "", 10);
1184
+ this._printedPageSize = Number.isInteger(printed) && printed > 0 ? printed : 0;
1185
+ } // The first query asks for the page the page printed (its page row's
1186
+ // page-size), unless the person kept another: the skeleton, the request
1187
+ // and the page row then agree on one number before the host answers.
1188
+
1189
+
1190
+ _seedPageSize() {
1191
+ if (this._pageSizeDeclared || this._answered) return;
1192
+
1193
+ const size = this._declaredPageSize();
1194
+
1195
+ if (!size) return;
1196
+ this.query.per_page = size;
1197
+ this._pageSizeDeclared = true;
1198
+ } // The host's first answer printed with the page (settings.first-answer,
1199
+ // a data-sf-settings island): the shape of query.change's applied data --
1200
+ // records (each with its revision), total or has_next, echo, columns. The
1201
+ // view draws it at once, as if the host had answered, so rows the server
1202
+ // already has are usable before the port; the port then answers the same
1203
+ // query in the background (refresh "connected", no loading state) and its
1204
+ // answer replaces this one. Taken once: a later answer is the port's.
1205
+
1206
+
1207
+ get firstAnswer() {
1208
+ return this._firstAnswer ?? null;
1209
+ }
1210
+
1211
+ set firstAnswer(data) {
1212
+ this._firstAnswer = data && typeof data === "object" && !Array.isArray(data) && Array.isArray(data.records) ? copy(data) : null; // Set after the view connected (not from its island, which is read
1213
+ // before the view is ready to draw): drawn now.
1214
+
1215
+ if (this._firstPaintReady && this.isConnected && this._firstAnswer && !this._answered) {
1216
+ this._drawFirstAnswer();
1217
+
1218
+ if (this.hostPort) this.refresh("connected");
1219
+ }
1220
+ }
1221
+
1222
+ _drawFirstAnswer() {
1223
+ const data = this._firstAnswer;
1224
+ if (!data || this._answered) return false;
1225
+ this._firstAnswer = null; // A printed answer to another query than the person's saved state is
1226
+ // not drawn: the skeleton stays and the view asks its own query.
1227
+
1228
+ if (!this._printedAnswerFits(data)) return false;
1229
+ if (data.echo && Number.isInteger(data.echo.per_page) && data.echo.per_page > 0) this._pageSizeDeclared = true;
1230
+ this._revalidate = true; // Kept to compare the port's answer to the same query with.
1231
+
1232
+ this._printedAnswer = copy(data); // The capabilities the page printed with its answer, when it did not
1233
+ // declare its port: known before the first draw (§18.4).
1234
+
1235
+ if (!this._declaredPort && Array.isArray(data.capabilities)) {
1236
+ this._declaredPort = {
1237
+ version: data.version ?? null,
1238
+ capabilities: data.capabilities.map(String)
1239
+ };
1240
+ }
1241
+
1242
+ const asked = copy(this.query);
1243
+ this.applyProjection(data); // The revalidation asks the query the page printed, not the answer's
1244
+ // echo: a host whose default (a sort) is echoed differently would
1245
+ // otherwise be asked another question (stand 27 D2). Without a printed
1246
+ // query it asks what the view would have asked.
1247
+
1248
+ if (!this._seedPrintedQuery({
1249
+ answer: data
1250
+ })) this.query = asked; // A printed skeleton the server sent as well goes with the first
1251
+ // answer, in the same frame.
1252
+
1253
+ this.removeAttribute("data-sf-awaiting");
1254
+
1255
+ for (const node of this._firstPaintNodes()) node.remove?.();
1256
+
1257
+ return true;
1258
+ } // The port the page declared in the element (standard §4.9, stand 27 c):
1259
+ // <script type="application/json" data-sf-port> with port, version, url,
1260
+ // capabilities and first {intent, payload}. Core may take it out and
1261
+ // connect the port itself; read here, it is what the view knows of its
1262
+ // port before the port: its capabilities, so the first draw is final
1263
+ // (the toolbar and the row actions drawn once), and its first query.
1264
+
1265
+
1266
+ _readDeclaredPort() {
1267
+ if (this._declaredPort !== undefined) return;
1268
+ const island = Array.from(this.children ?? []).find(child => child.localName === "script" && child.hasAttribute?.("data-sf-port") && /^application\/json$/iu.test(child.type));
1269
+ this._declaredPort = null;
1270
+ if (!island) return;
1271
+
1272
+ try {
1273
+ const declared = JSON.parse(island.textContent || "{}");
1274
+ if (!declared || typeof declared !== "object") return;
1275
+ this._declaredPort = {
1276
+ version: declared.version ?? null,
1277
+ capabilities: Array.isArray(declared.capabilities) ? declared.capabilities.map(String) : [],
1278
+ first: declared.first && typeof declared.first === "object" ? copy(declared.first) : null
1279
+ };
1280
+ } catch (error) {
1281
+ console.warn("[sf-view] the data-sf-port island is not JSON", error);
1282
+ }
1283
+ } // The port as the view knows it: the connected one, else the one the page
1284
+ // declared (or printed beside its first answer).
1285
+
1286
+
1287
+ get knownPort() {
1288
+ return this.hostPort ?? this._declaredPort ?? null;
1289
+ }
1290
+
1291
+ knownCapability(capability) {
1292
+ return Boolean(this.knownPort?.capabilities?.includes?.(capability));
1293
+ } // The query the page printed: the first answer's own (first-answer.query),
1294
+ // else the declared port's first query.change. The view asks exactly that,
1295
+ // so an early call the page started is the view's own (one request) and
1296
+ // the revalidation of a printed answer asks what was printed.
1297
+
1298
+
1299
+ _seedPrintedQuery({
1300
+ answer = null
1301
+ } = {}) {
1302
+ const declared = this._declaredPort?.first;
1303
+ const printed = answer?.query && typeof answer.query === "object" ? answer.query : declared?.intent === "query.change" && declared.payload?.query && typeof declared.payload.query === "object" ? declared.payload.query : null;
1304
+ if (!printed) return false; // The printed query is the defaults; what the person saved in this
1305
+ // browser goes over it. The view asks its real state: an early call
1306
+ // started with the printed query is then taken only when it matches
1307
+ // (the adapter compares), otherwise the view's own query is asked.
1308
+
1309
+ this.query = { ...this.query,
1310
+ ...copy(printed),
1311
+ ...copy(this._keptState ?? {})
1312
+ };
1313
+ if (Number.isInteger(this.query.per_page) && this.query.per_page > 0) this._pageSizeDeclared = true;
1314
+ return true;
1315
+ } // Whether a printed answer answers the view's real query: not when the
1316
+ // person saved a page size or a sort in this browser that the printed
1317
+ // query (or the answer's echo) does not have. Drawn anyway, its rows
1318
+ // would show and then be replaced by the real query's (25 rows, then 50).
1319
+
1320
+
1321
+ _printedAnswerFits(data) {
1322
+ const kept = this._keptState ?? {};
1323
+ if (!Object.keys(kept).length) return true;
1324
+ const printed = data?.query && typeof data.query === "object" ? data.query : data?.echo ?? {};
1325
+ if (kept.per_page !== undefined && Number(printed.per_page) !== kept.per_page) return false;
1326
+ if (kept.sort !== undefined && canonical(printed.sort ?? []) !== canonical(kept.sort)) return false;
1327
+ return true;
1328
+ } // How many columns the skeleton stands for (expected-columns): the shape
1329
+ // of the first paint, never a limit -- the host's answer brings its own
1330
+ // columns. The page or server that renders a collection knows them.
1331
+
1332
+
1333
+ get expectedColumns() {
1334
+ const declared = Number.parseInt(this.getAttribute("expected-columns") ?? "", 10);
1335
+ return Number.isFinite(declared) && declared > 0 ? declared : 4;
1336
+ } // Until the host's first answer the view shows its skeleton: the shape the
1337
+ // list will have once the answer comes, so nothing moves when it does
1338
+ // (standard 1.3.8 §5.4, gate 24). The toolbar and the page row as the data
1339
+ // draws them; the table's head and a row per row it will draw (row-cap,
1340
+ // else a page of 10), a cell per expected column, with the chrome columns
1341
+ // (select, settings) the table will have; and the compact list the table
1342
+ // turns into below its list-below width. The table chooses that in code,
1343
+ // by measuring its container; a printed skeleton cannot measure, so the
1344
+ // choice is the sheet's: a container query at the same width (sf-view-table's
1345
+ // sheet, .sf-data-view-skeleton). It is light DOM, so a server prints it
1346
+ // as the view's static markup and the page draws it before any code; the
1347
+ // view adopts what was printed (its data-sf-static mark goes) rather than
1348
+ // drawing a second one beside it.
1349
+
1350
+
1351
+ _startFirstPaint() {
1352
+ if (this._answered) return;
1353
+ this.setAttribute("data-sf-awaiting", "");
1354
+ if (this.getAttribute("aria-busy") === null) this.setAttribute("aria-busy", "true");
1355
+
1356
+ const printed = this._firstPaintNodes();
1357
+
1358
+ if (printed.length) {
1359
+ for (const node of printed) node.removeAttribute?.("data-sf-static");
1360
+
1361
+ return;
1362
+ }
1363
+
1364
+ if (typeof document === "undefined") return;
1365
+ const skeleton = document.createElement("div");
1366
+ skeleton.setAttribute("slot", "first-paint");
1367
+ skeleton.setAttribute("aria-hidden", "true");
1368
+ skeleton.className = "sf-data-view-skeleton sf-skeleton sf-skeleton--animation-pulse";
1369
+ skeleton.setAttribute("data-size", this.size);
1370
+ skeleton.innerHTML = this._firstPaintMarkup(skeleton);
1371
+ this.append(skeleton);
1372
+ } // The skeleton's markup, kept small (it is printed in every page that
1373
+ // shows the view): a bar is an <i class="sf-skeleton-text">, a control
1374
+ // block one marked data-control; the lengths and the bars' widths are the
1375
+ // sheet's (sf-view-table's sheet, .sf-data-view-skeleton).
1376
+
1377
+
1378
+ _firstPaintMarkup(skeleton) {
1379
+ const table = this.querySelector(`:scope > ${(0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view-table")}[slot="content"]`);
1380
+ const bar = '<i class="sf-skeleton-text"></i>';
1381
+
1382
+ const control = (kind = "") => `<i class="sf-skeleton-text" data-control${kind ? `="${kind}"` : ""}></i>`; // What the data will draw: the table's toolbar unless the page turned
1383
+ // it off (without a table of the page's, the view's own as its chrome
1384
+ // asks), the chrome columns, and a table or the compact list.
1385
+
1386
+
1387
+ const toolbar = table ? table.getAttribute("toolbar") !== "false" : this.renders("toolbar");
1388
+ const ownToolbar = Boolean(table) && this.renders("toolbar") && Boolean(this.querySelector(':scope > [slot="toolbar"]'));
1389
+ const select = this.renders("select") && !hostWroteOff(table, "selectable");
1390
+ const settings = this.renders("settings") && !hostWroteOff(table, "settings");
1391
+ const layout = table?.getAttribute("layout") || (this.renders("toolbar") ? "auto" : "table");
1392
+ skeleton.setAttribute("data-layout", ["auto", "table", "list"].includes(layout) ? layout : "table");
1393
+ const columns = this.expectedColumns;
1394
+ const rows = this.firstPaintRows;
1395
+ const listFields = Number.parseInt(table?.getAttribute("list-fields") ?? "", 10);
1396
+ const fields = Math.min(columns - 1, Number.isFinite(listFields) && listFields >= 0 ? listFields : FIRST_PAINT_LIST_FIELDS);
1397
+
1398
+ const row = head => `<div>${select ? `<span data-chrome>${bar}</span>` : ""}${settings ? `<span data-chrome>${head ? bar : control()}</span>` : ""}${`<span>${bar}</span>`.repeat(columns)}</div>`;
1399
+
1400
+ const record = `<div>${select ? bar : "<span></span>"}<div>${bar.repeat(fields + 1)}</div>${settings ? control() : "<span></span>"}</div>`;
1401
+ const pageRow = `<nav class="sf-pagination sf-pagination--size-${this.size} flex flex-col"><div class="sf-pagination-main flex items-cross-center content-main-between">` + `<div class="sf-pagination-left flex items-cross-center">${bar}</div>` + `<div class="sf-pagination-container flex items-cross-center"><span class="sf-pagination-pages-label">${bar}</span>${control()}</div>` + `<div class="sf-pagination-count flex items-cross-center">${bar}${control("field")}</div>` + "</div></nav>";
1402
+ return [// An own toolbar slot is a row of the view's layout, even empty.
1403
+ ownToolbar ? '<div class="sf-data-view-skeleton-own-toolbar"></div>' : "", '<div class="sf-data-view-skeleton-records">', toolbar ? `<div class="sf-data-view-skeleton-toolbar">${control("button")}${control("field")}</div>` : "", `<div class="sf-data-view-skeleton-table">${row(true)}${Array.from({
1404
+ length: rows
1405
+ }, () => row(false)).join("")}</div>`, `<div class="sf-data-view-skeleton-list">${record.repeat(rows)}</div>`, "</div>", this.renders("pagination") ? pageRow : ""].join("");
1406
+ } // The host answered (a list, an empty one, a refusal or a failure): the
1407
+ // skeleton goes and the layout comes, in one frame.
1408
+
1409
+
1410
+ _endFirstPaint() {
1411
+ if (this.getAttribute("data-sf-awaiting") === null) return;
1412
+ this.removeAttribute?.("data-sf-awaiting");
1413
+
1414
+ for (const node of this._firstPaintNodes()) node.remove?.();
1415
+ }
1416
+
1417
+ _firstPaintNodes() {
1418
+ return Array.from(this.children ?? []).filter(child => child.getAttribute?.("slot") === "first-paint");
1419
+ } // A component that takes a host port can also ask for one, so a consumer who
1420
+ // composes it inside something else does not have to find the element and call
1421
+ // a method on it. Raised once per connection and only while there is no port;
1422
+ // `setHostPort` is what answers it, synchronously or later. The name is the
1423
+ // shared convention rather than one of this element's own: the next composite
1424
+ // that takes a host port raises the same event, and a host answers them all
1425
+ // the same way.
1426
+
1427
+
1428
+ _requestHostPort() {
1429
+ if (this.hostPort || this._portRequested) return false;
1430
+ this._portRequested = true;
1431
+ this.dispatchEvent(new CustomEvent("sf-host-port-request", {
1432
+ bubbles: true,
1433
+ composed: true,
1434
+ detail: {
1435
+ component: this,
1436
+ context: {
1437
+ owner: this.portOwner,
1438
+ record: this.portRecord,
1439
+ group: this.portGroup
1440
+ }
1441
+ }
1442
+ }));
1443
+ return true;
1444
+ } // What a host needs to know to answer: which composite is asking and about
1445
+ // what. Set by whatever embedded this element; absent on a page list, which is
1446
+ // asking about nothing but itself.
1447
+
1448
+
1449
+ get portOwner() {
1450
+ return this.getAttribute("port-owner") || null;
1451
+ }
1452
+
1453
+ get portRecord() {
1454
+ return this.getAttribute("port-record") || null;
1455
+ }
1456
+
1457
+ get portGroup() {
1458
+ return this.getAttribute("port-group") || null;
1459
+ }
1460
+
1461
+ disconnectedCallback() {
1462
+ this.epoch += 1; // Once per connection, so an element moved in the DOM asks again. Without
1463
+ // this reset a list re-attached into a different host would sit portless
1464
+ // and silent, which looks exactly like a host that chose not to answer.
1465
+
1466
+ this._portRequested = false;
1467
+ this._controller?.abort();
1468
+ this._controller = null;
1469
+ const events = [...Object.keys(EVENTS), ...LISTENED];
1470
+
1471
+ for (const name of events) this.shadowRoot.removeEventListener(name, this._bound);
1472
+
1473
+ for (const name of events) this.removeEventListener(name, this._bound);
1474
+
1475
+ this.shadowRoot.querySelector("[data-search]").removeEventListener("change", this._bound);
1476
+ this.shadowRoot.querySelector("[data-view]").removeEventListener("change", this._bound);
1477
+ this.shadowRoot.querySelector("[data-save]").removeEventListener("click", this._bound);
1478
+ this.shadowRoot.querySelector("[data-delete]").removeEventListener("click", this._bound);
1479
+ this.pending = null;
1480
+ } // A host adapter exposes raise(intent, payload, {signal}). No transport is implied.
1481
+ // Which of this element's own parts render. A list and not a boolean, because
1482
+ // a boolean would have to mean «all of it or none», and an embedded list wants
1483
+ // its rows without a search box while a page list wants both. The names are
1484
+ // the ones this element already publishes as parts and slots, so nothing here
1485
+ // gives an existing thing a second name -- `sf-view-table` keeps its own `toolbar`
1486
+ // and `sf-pagination` keeps `top`/`bottom`.
1487
+
1488
+
1489
+ get chrome() {
1490
+ const declared = this.getAttribute("chrome");
1491
+ if (declared === null) return [...CHROME_PARTS];
1492
+ return declared.split(/\s+/u).filter(part => CHROME_PARTS.includes(part));
1493
+ }
1494
+
1495
+ renders(part) {
1496
+ return this.chrome.includes(part);
1497
+ } // How many rows an embedded list draws at most. Zero is no cap, which is what
1498
+ // a page list is. Not `limit`, which reads like a query limit the host owns,
1499
+ // and not `page-size`, which the pagination and the table already use for the
1500
+ // host's paging -- a cap is what this element shows of a page it already has.
1501
+
1502
+
1503
+ get rowCap() {
1504
+ const declared = Number.parseInt(this.getAttribute("row-cap") ?? "", 10);
1505
+ return Number.isFinite(declared) && declared > 0 ? declared : 0;
1506
+ }
1507
+
1508
+ get cappedRecords() {
1509
+ // A tree is drawn whole, as far as it is open: the cap is a list's.
1510
+ if (this.treeMode === "tree") return (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.visibleRows)(this.tree);
1511
+ const cap = this.rowCap;
1512
+ return cap ? this.records.slice(0, cap) : this.records;
1513
+ }
1514
+
1515
+ get capHidesRows() {
1516
+ const cap = this.rowCap;
1517
+ return Boolean(cap) && (this.records.length > cap || this.hasNext === true);
1518
+ } // The whole list is the host's to open. The component has no address to build
1519
+ // one from, and a route it guessed would send a person to a page the product
1520
+ // may not have. The query travels so the full view opens on the records the
1521
+ // person was looking at rather than on an unfiltered collection.
1522
+
1523
+
1524
+ openList(reason = "row-cap") {
1525
+ if (!this.hostPort) return Promise.resolve({
1526
+ answer: "unavailable",
1527
+ reason: "port-not-connected"
1528
+ });
1529
+ return this._raise("list.open", {
1530
+ reason,
1531
+ query: {
1532
+ search: this.query.search ?? "",
1533
+ filters: { ...(this.query.filters ?? {})
1534
+ }
1535
+ }
1536
+ });
1537
+ } // The presentation attributes are observed, because they are read when the
1538
+ // projection runs and nothing re-ran it. A consumer rendering this element
1539
+ // through a template sets its attributes after it is in the document, so the
1540
+ // first projection saw no `chrome` at all and drew the full page chrome -- a
1541
+ // consumer reported the fallback «Поиск / Сохранить вид» form standing inside a
1542
+ // group that had asked for no chrome, until data arrived and projected again.
1543
+ // The step of the size ladder the view's controls take, handed to the table,
1544
+ // the page row and the selection band: 1, or the admin's dense 1/2, where the
1545
+ // view stands beside the admin menu's 32px rows (owner, 2026-10-13).
1546
+
1547
+
1548
+ get size() {
1549
+ return this.getAttribute("size") === "1/2" ? "1/2" : "1";
1550
+ }
1551
+
1552
+ static get observedAttributes() {
1553
+ return ["chrome", "row-cap", "aria-label", "size", "preferences-key", ...LABEL_ATTRIBUTES];
1554
+ } // The words of the table and the pagination the view draws itself, written
1555
+ // on the view and passed on: a host that let the view draw its own table
1556
+ // had no way to word it, so a list inside the CMS settings spoke the
1557
+ // fallback words (Larena, 2026-10-13). A word the view is not given is left
1558
+ // to the part's own default; one that is removed is removed there too.
1559
+
1560
+
1561
+ _forwardLabels() {
1562
+ this._forwarded ??= new Set();
1563
+
1564
+ const pass = (target, from, to = from) => {
1565
+ if (!target?.setAttribute) return;
1566
+ const value = this.getAttribute(from);
1567
+ const key = `${target.localName}:${to}`;
1568
+
1569
+ if (value === null) {
1570
+ if (this._forwarded.delete(key)) target.removeAttribute(to);
1571
+ return;
1572
+ }
1573
+
1574
+ target.setAttribute(to, value);
1575
+
1576
+ this._forwarded.add(key);
1577
+ };
1578
+
1579
+ for (const name of TABLE_LABELS) pass(this.table, name);
1580
+
1581
+ for (const name of PAGINATION_LABELS) pass(this.pagination, name);
1582
+
1583
+ pass(this.pagination, "pagination-aria-label", "aria-label");
1584
+ }
1585
+
1586
+ attributeChangedCallback(name, previous, next) {
1587
+ if (previous === next || !this.isConnected) return;
1588
+ if (name === "preferences-key") this._applyPreferences();
1589
+
1590
+ this._renderProjection();
1591
+ }
1592
+
1593
+ setHostPort(host) {
1594
+ if (!host || typeof host.raise !== "function" || !SUPPORTED_PORT_VERSIONS.includes(host.version)) {
1595
+ throw new TypeError(`sf-view requires a declared dataview host port: ${SUPPORTED_PORT_VERSIONS.join(", ")}`);
1596
+ }
1597
+
1598
+ this.hostPort = host;
1599
+
1600
+ this._applyForAll();
1601
+
1602
+ if (this.isConnected) this.refresh("host-connected");
1603
+ return this;
1604
+ } // «Для всех (N)»: the page row offers the filter-wide action when the host
1605
+ // can act on all (bulk-under-filter), on the view's own page row and on a
1606
+ // page's that does not say otherwise (stand 24 P2: the box never showed,
1607
+ // since nothing set show-action-for-all). A page that writes the
1608
+ // attribute decides; what the view wrote it takes back when the host
1609
+ // stops offering it.
1610
+
1611
+
1612
+ _applyForAll() {
1613
+ const pagination = this.pagination;
1614
+ if (!pagination?.setAttribute) return;
1615
+ const mine = pagination.getAttribute("data-sf-view-for-all") !== null;
1616
+ if (!mine && pagination.getAttribute("show-action-for-all") !== null) return;
1617
+
1618
+ if (this.hostOffers("bulk-under-filter")) {
1619
+ if (!mine) {
1620
+ pagination.setAttribute("show-action-for-all", "true");
1621
+ pagination.setAttribute("data-sf-view-for-all", "");
1622
+ }
1623
+ } else if (mine) {
1624
+ pagination.removeAttribute?.("show-action-for-all");
1625
+ pagination.removeAttribute?.("data-sf-view-for-all");
1626
+ }
1627
+ } // `message` is for the live region only: it is announced, never laid out.
1628
+
1629
+
1630
+ _setState(state, message = "") {
1631
+ this._dataState = state;
1632
+ if (state !== "loading") this._endFirstPaint();
1633
+ this.setAttribute("data-state", state);
1634
+ this.setAttribute("aria-busy", state === "loading" ? "true" : "false");
1635
+ if (this.table && state === "empty") this.table.emptyContext = this._emptyContext(); // A load the person caused -- a search or a filter they set -- that
1636
+ // fails is an alert; any other failed load is said politely.
1637
+
1638
+ if (this.table && state === "error") {
1639
+ this.table.emptyContext = {
1640
+ caused: ["search", "filter"].includes(this._queryReason),
1641
+ ...(this._queryMessage ? {
1642
+ message: this._queryMessage
1643
+ } : {})
1644
+ };
1645
+ }
1646
+
1647
+ this.table?.setDataState?.(state); // A failed or refused list has nothing to count: the page row goes with
1648
+ // it until a list comes back (docs trial 4: «Всего: 10» over an error).
1649
+
1650
+ const pageRow = this.shadowRoot.querySelector('slot[name="pagination"]');
1651
+ if (pageRow && ["error", "no-access"].includes(state)) pageRow.hidden = true;
1652
+ const status = this.shadowRoot.querySelector('[part="status"]');
1653
+ status.hidden = !message;
1654
+ status.textContent = message;
1655
+ this.dispatchEvent(new CustomEvent("sf-view-state", {
1656
+ bubbles: true,
1657
+ composed: true,
1658
+ detail: {
1659
+ state,
1660
+ message,
1661
+ instance_id: this.instanceId
1662
+ }
1663
+ }));
1664
+ } // Why the list is empty, from what the view already knows (owner,
1665
+ // 2026-10-14): the host refused the query -- no access; a search or a
1666
+ // filter is on -- nothing found, and what a reset would clear; otherwise
1667
+ // no data. The table draws it.
1668
+
1669
+
1670
+ _emptyContext() {
1671
+ if (this._queryRefused) return this._queryMessage ? {
1672
+ kind: "no-access",
1673
+ message: this._queryMessage
1674
+ } : {
1675
+ kind: "no-access"
1676
+ };
1677
+ const search = String(this.query.search ?? "").trim() !== "";
1678
+ const filters = hasValue(this.query.filters);
1679
+ if (search || filters) return {
1680
+ kind: "nothing-found",
1681
+ reset: search && filters ? "both" : search ? "search" : "filters"
1682
+ };
1683
+ return {
1684
+ kind: "no-data"
1685
+ };
1686
+ } // Whether the host offers a capability: announced, or -- for the two that
1687
+ // were baseline before port 1.10.0 -- implied by an earlier version.
1688
+
1689
+
1690
+ hostOffers(capability) {
1691
+ // The connected port, else the one the page declared (knownPort).
1692
+ const port = this.hostPort ?? this._declaredPort ?? null;
1693
+ if (!port) return false;
1694
+ if (port.capabilities?.includes?.(capability)) return true;
1695
+ return BASELINE_UNTIL_1_10.has(capability) && !portAtLeast(port.version, "1.10.0");
1696
+ }
1697
+
1698
+ async _raise(intent, payload, {
1699
+ query = false
1700
+ } = {}) {
1701
+ if (!this.hostPort) {
1702
+ if (query) this._queryMessage = "";
1703
+
1704
+ this._setState("error", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "statePortNotConnected", this));
1705
+
1706
+ this.notify((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "noticePortNotConnected", this));
1707
+ return {
1708
+ answer: "unavailable",
1709
+ reason: "port-not-connected"
1710
+ };
1711
+ }
1712
+
1713
+ const capability = {
1714
+ "view.open": "saved-views",
1715
+ "view.save": "saved-views",
1716
+ "view.delete": "saved-views",
1717
+ "view.save_shared": "shared-saved-views",
1718
+ "settings.save_shared": "shared-column-settings",
1719
+ "bulk.apply_under_filter": "bulk-under-filter",
1720
+ "record.open": "row-actions",
1721
+ "record.mutate": "row-actions",
1722
+ "tree.expand": "tree",
1723
+ "node.targets": "tree-move",
1724
+ "node.move": "tree-move",
1725
+ "view.create_record": "create-record",
1726
+ "settings.save_personal": "personal-column-settings",
1727
+ "record.reorder": "row-reorder",
1728
+ "record.set": "inline-edit"
1729
+ }[intent];
1730
+
1731
+ if (capability && !this.hostOffers(capability)) {
1732
+ const answer = {
1733
+ answer: "unavailable",
1734
+ reason: "capability-not-provided"
1735
+ };
1736
+
1737
+ this._showAnswer(intent, answer);
1738
+
1739
+ return answer;
1740
+ } // The tree's intents are announced as the one event the port names
1741
+ // for them, so a page can watch them without standing in the way.
1742
+
1743
+
1744
+ if (TREE_INTENTS.has(intent)) {
1745
+ // sf-data-view-tree; the old sf-table-tree-intent follows once (aliases.json).
1746
+ this.sfEmit("tree", {
1747
+ intent,
1748
+ payload: copy(payload)
1749
+ });
1750
+ }
1751
+
1752
+ const epoch = this.epoch;
1753
+ const sequence = query ? payload.sequence : null;
1754
+
1755
+ try {
1756
+ const answer = await this.hostPort.raise(intent, copy(payload), {
1757
+ signal: this._controller?.signal
1758
+ });
1759
+ if (!this.isConnected || epoch !== this.epoch || query && sequence !== this.sequence) return null;
1760
+
1761
+ if (!answer || !["applied", "conflict", "refused", "unavailable"].includes(answer.answer)) {
1762
+ throw new TypeError("Host answer is not a declared port answer");
1763
+ }
1764
+
1765
+ if (query && answer.sequence != null && answer.sequence !== sequence) return null;
1766
+ if (query) this.pending = null; // Refused for access: «no access», without the toolbar. Refused for
1767
+ // the query: the query stays on screen to be corrected (isAccessRefusal).
1768
+
1769
+ if (query) this._queryRefused = answer.answer === "refused" && isAccessRefusal(answer.reason); // The host's own sentence for a list it cannot give or will not
1770
+ // give (Larena, 2026-10-21: «Список готовится…» while a large
1771
+ // storage's read model is built). It is shown as the state's own
1772
+ // words, as text: it is the host's, and never markup.
1773
+
1774
+ if (query) this._queryMessage = ["refused", "unavailable"].includes(answer.answer) ? hostMessage(answer) : ""; // A write the host confirms first (port 1.8.0): it answered with what
1775
+ // it is about to do and a digest of it. The person reads it; if they
1776
+ // confirm, the same intent goes again with the digest, which the
1777
+ // host checks against the write it described.
1778
+
1779
+ if (!query && answer.answer === "refused" && answer.reason === "confirm_required" && answer.data?.confirm?.digest && !payload?.confirm) {
1780
+ await this._ensureElements(["sf-button"]);
1781
+ const confirmed = await this._dialog({
1782
+ title: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "confirmTitle", this),
1783
+ text: String(answer.data.confirm.summary ?? ""),
1784
+ apply: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "confirmApply", this)
1785
+ });
1786
+ if (!confirmed) return {
1787
+ answer: "refused",
1788
+ reason: "not-confirmed"
1789
+ };
1790
+ return this._raise(intent, { ...payload,
1791
+ confirm: String(answer.data.confirm.digest)
1792
+ });
1793
+ }
1794
+
1795
+ if (answer.answer === "applied") {
1796
+ // Where a node may go is an answer to a question, not a view of
1797
+ // the list: it is read by the move dialog and changes no rows.
1798
+ if (ANSWER_ONLY.has(intent)) {// nothing to project
1799
+ } else if (!query && this._isRecordPatch(answer.data)) this._applyRecordPatch(answer.data.records);else if (answer.data) this.applyProjection(answer.data);
1800
+
1801
+ if (query && !answer.data) throw new TypeError("Applied query has no data");
1802
+ if (intent === "view.open") this.refresh("view-switch"); // A write that changed no record cannot have changed the rows.
1803
+ // Column settings — order, width, visibility, pinning — used to
1804
+ // fall into this branch and re-query the whole page, so moving a
1805
+ // column reloaded the view and flashed a loader at the person.
1806
+
1807
+ if (!query && !answer.data && !NO_REFRESH_AFTER.has(intent)) this.refresh("after-write"); // What the write changed for the rest of the page goes out on
1808
+ // the page host's outputs (simai.interface-fragment, effects);
1809
+ // the page's routes take it to whoever listens.
1810
+
1811
+ if (Array.isArray(answer.effects) && answer.effects.length) globalThis.SF?.Composition?.publishEffects?.(answer.effects, {
1812
+ origin: this
1813
+ });
1814
+ } else if (query && answer.answer === "refused" && !this._queryRefused) {
1815
+ // A query the host would not answer as asked -- a search
1816
+ // longer than it takes, a filter it does not allow. The rows of
1817
+ // the last answer do not answer this query, so they go; the
1818
+ // toolbar, the search the person typed and the filters stay,
1819
+ // and the state says what went wrong: the host's sentence as
1820
+ // text, or the view's own words. No notice over it: the state
1821
+ // and the live region already say it.
1822
+ this.records = [];
1823
+ this.appended = null;
1824
+ this.total = 0;
1825
+ this.countKnown = false;
1826
+
1827
+ this._renderProjection(); // The brick's title as well as the live region: the table's own
1828
+ // «could not load» would say the wrong thing about a query.
1829
+
1830
+
1831
+ if (!this._queryMessage) this._queryMessage = (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "stateQueryFailed", this);
1832
+
1833
+ this._setState("error", this._queryMessage);
1834
+
1835
+ this._showAnswer(intent, answer, {
1836
+ stated: true
1837
+ });
1838
+
1839
+ return answer;
1840
+ } else if (query && answer.answer === "refused") {
1841
+ // A refused list shows nothing it had: the rows of the last
1842
+ // allowed answer stayed under «no access» (docs trial 4).
1843
+ this.records = [];
1844
+ this.appended = null;
1845
+ this.total = 0;
1846
+ this.countKnown = false;
1847
+
1848
+ this._renderProjection();
1849
+ } else if (query && answer.answer === "unavailable") {
1850
+ // A host that says why the list is not there is believed, and
1851
+ // its sentence is the state: the error brick and the live
1852
+ // region say it, and no error notice says something else over
1853
+ // it. Without one, the general words and the notice as before.
1854
+ this._setState("error", this._queryMessage || (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "stateUnavailable", this));
1855
+
1856
+ if (!this._queryMessage) this.notify((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "noticeFetchFailed", this), {
1857
+ code: answer.reason || ""
1858
+ });
1859
+ }
1860
+
1861
+ this._showAnswer(intent, answer);
1862
+
1863
+ return answer;
1864
+ } catch (error) {
1865
+ if (!this.isConnected || epoch !== this.epoch) return null;
1866
+ const answer = {
1867
+ answer: "unavailable",
1868
+ reason: error?.message || "port-error"
1869
+ };
1870
+
1871
+ if (query) {
1872
+ this._queryMessage = "";
1873
+
1874
+ this._setState("error", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "stateLoadFailed", this));
1875
+
1876
+ this.notify((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "noticeLoadFailed", this), {
1877
+ code: answer.reason || ""
1878
+ });
1879
+ }
1880
+
1881
+ this._showAnswer(intent, answer);
1882
+
1883
+ return answer;
1884
+ }
1885
+ }
1886
+
1887
+ _showAnswer(intent, answer, {
1888
+ stated = false
1889
+ } = {}) {
1890
+ if (!stated && ["conflict", "refused"].includes(answer.answer) || answer.answer === "unavailable" && intent !== "query.change") {
1891
+ const text = this._noticeText(answer);
1892
+
1893
+ this._setState(this.records.length ? "populated" : "empty", text);
1894
+
1895
+ this.notify(text, {
1896
+ kind: answer.answer === "conflict" ? "warning" : "error",
1897
+ code: answer.reason || ""
1898
+ });
1899
+ }
1900
+
1901
+ this.dispatchEvent(new CustomEvent("sf-view-answer", {
1902
+ bubbles: true,
1903
+ composed: true,
1904
+ detail: {
1905
+ intent,
1906
+ ...copy(answer),
1907
+ instance_id: this.instanceId
1908
+ }
1909
+ }));
1910
+ } // The key the person's view of this list is kept under: the host names
1911
+ // it (preferences-key, e.g. the collection). The table takes the same.
1912
+
1913
+
1914
+ get preferencesKey() {
1915
+ const key = this.getAttribute("preferences-key") || "";
1916
+ return key ? viewPreferenceKey(key) : "";
1917
+ } // Before the first query: the person's sort and page size, so the rows
1918
+ // come in their order the first time rather than after a click.
1919
+ // A list page scrolls as a page (owner, 2026-10-15): the view's table is in
1920
+ // page mode unless the host chose its mode itself.
1921
+
1922
+
1923
+ _tableScrollMode(table) {
1924
+ if (typeof table?.hasAttribute !== "function" || typeof table.setAttribute !== "function") return;
1925
+
1926
+ if (!table.hasAttribute("data-sf-host-vertical-scroll")) {
1927
+ table.setAttribute("data-sf-host-vertical-scroll", table.getAttribute("vertical-scroll") ?? "");
1928
+ }
1929
+
1930
+ if (!table.getAttribute("data-sf-host-vertical-scroll")) table.setAttribute("vertical-scroll", "page");
1931
+ }
1932
+
1933
+ _applyPreferences() {
1934
+ this._tableScrollMode(this.table);
1935
+
1936
+ const key = this.preferencesKey;
1937
+ if (key === this._preferencesApplied) return;
1938
+ this._preferencesApplied = key;
1939
+ const table = this.table;
1940
+ if (table && key && !table.getAttribute("preferences-key")) table.setAttribute("preferences-key", this.getAttribute("preferences-key"));
1941
+ const kept = key ? preferences()?.read(key) : null; // What the person saved in this browser: it wins over anything the
1942
+ // page printed (a declared port's first query, a printed answer's
1943
+ // query), which the page wrote without knowing it (standard §4.9:
1944
+ // the page must not guess).
1945
+
1946
+ this._keptState = { ...(Array.isArray(kept?.sort) && kept.sort.length ? {
1947
+ sort: copy(kept.sort)
1948
+ } : {}),
1949
+ ...(Number.isInteger(kept?.pageSize) && kept.pageSize > 0 ? {
1950
+ per_page: kept.pageSize
1951
+ } : {})
1952
+ };
1953
+ if (!kept) return;
1954
+ if (Array.isArray(kept.sort) && !this.query.sort?.length) this.query.sort = copy(kept.sort);
1955
+
1956
+ if (Number.isInteger(kept.pageSize) && kept.pageSize > 0) {
1957
+ this.query.per_page = kept.pageSize;
1958
+ this._pageSizeDeclared = true;
1959
+ }
1960
+ }
1961
+
1962
+ _keepPreference(patch) {
1963
+ const key = this.preferencesKey;
1964
+ if (!key) return;
1965
+ preferences()?.write(key, { ...(preferences()?.read(key) ?? {}),
1966
+ v: 1,
1967
+ ...patch
1968
+ });
1969
+ }
1970
+
1971
+ refresh(reason = "refresh") {
1972
+ if (!this.isConnected) return Promise.resolve(null);
1973
+
1974
+ this._applyPreferences();
1975
+
1976
+ const sequence = ++this.sequence;
1977
+ this.pending = sequence;
1978
+ this._queryReason = reason; // Show more continues the list; every other reason replaces it. The
1979
+ // answer carries no reason of its own, so it is remembered here.
1980
+
1981
+ this.continuing = reason === "show-more"; // The port's first answer over a first answer the page printed is a
1982
+ // revalidation: the rows stay as they are, usable, until it comes.
1983
+
1984
+ const revalidating = this._revalidate === true && ["connected", "host-connected"].includes(reason);
1985
+ this._revalidate = false;
1986
+ this._revalidating = revalidating; // Continuing the list is answered inside the button that asked for it.
1987
+ // The table is not loading — the rows on screen stay exactly as they
1988
+ // are — so it neither dims them nor replaces them with a loader.
1989
+
1990
+ if (this.continuing) {
1991
+ this.pagination?.setState?.({
1992
+ showMoreBusy: true
1993
+ });
1994
+ } else if (!revalidating) {
1995
+ this._setState("loading", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "stateLoading", this));
1996
+ }
1997
+
1998
+ return this._raise("query.change", {
1999
+ sequence,
2000
+ query: copy(this.query),
2001
+ reason
2002
+ }, {
2003
+ query: true
2004
+ });
2005
+ }
2006
+
2007
+ _syncSearchBoxes(value) {
2008
+ const text = String(value ?? "");
2009
+ const own = this.shadowRoot?.querySelector("input[data-search]");
2010
+ if (own && own.value !== text && this.shadowRoot.activeElement !== own) own.value = text;
2011
+ const table = this.table;
2012
+
2013
+ if (table?.updateSearchState && (table.state?.search?.value ?? "") !== text && !table.contains(document.activeElement)) {
2014
+ table.updateSearchState({
2015
+ value: text
2016
+ }, false);
2017
+ }
2018
+ }
2019
+
2020
+ changeQuery(patch, reason = "query-change") {
2021
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw new TypeError("Query patch must be an object");
2022
+ this.query = { ...this.query,
2023
+ ...copy(patch)
2024
+ };
2025
+ if (!["page-change", "page-size-change", "show-more"].includes(reason)) this.query.page = 1;
2026
+ return this.refresh(reason);
2027
+ }
2028
+
2029
+ applyProjection(data) {
2030
+ if (!data || typeof data !== "object") throw new TypeError("Port data must be an object"); // The port's answer to the query a printed first answer already
2031
+ // answered (stand 25, Larena's storage page: the printed list was drawn
2032
+ // and then drawn again, CLS 0.13 -> 0.32). The same answer -- the same
2033
+ // revision, or the same rows, count and columns -- changes nothing on
2034
+ // the page; a different one is taken in place: the rows keyed by id,
2035
+ // only the changed ones redrawn, in one render, no skeleton between.
2036
+
2037
+ let inPlace = false;
2038
+
2039
+ if (this._revalidating) {
2040
+ this._revalidating = false;
2041
+ const printed = this._printedAnswer;
2042
+ this._printedAnswer = null;
2043
+
2044
+ if (printed && sameAnswer(printed, data)) {
2045
+ // Only what the port itself brings is drawn: its capabilities
2046
+ // (row actions, settings), when they differ from the ones the
2047
+ // printed answer was drawn without.
2048
+ if (this._capabilitiesKey !== capabilitiesKey(this.knownPort)) this._renderProjection();
2049
+ return;
2050
+ }
2051
+
2052
+ inPlace = Boolean(printed);
2053
+ }
2054
+
2055
+ if (Array.isArray(data.row_actions)) this.rowActionCatalog = copy(data.row_actions); // One node's children: they go under it, and nothing else about the
2056
+ // view changes -- not the query, not the count, not the columns.
2057
+
2058
+ const tree = data.tree && typeof data.tree === "object" ? data.tree : null;
2059
+
2060
+ if (tree?.mode === "tree" && tree.parent !== null && tree.parent !== undefined && Array.isArray(data.records)) {
2061
+ const parent = String(tree.parent);
2062
+ (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.acceptLevel)(this.tree, {
2063
+ parent,
2064
+ records: copy(data.records),
2065
+ hasNext: tree.has_next === true,
2066
+ cursor: tree.cursor ?? null,
2067
+ append: this._treeAppend.delete(parent)
2068
+ });
2069
+ this.records = (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.knownRecords)(this.tree);
2070
+
2071
+ this._renderProjection();
2072
+
2073
+ return;
2074
+ } // An answer without records is no new shape of the list (a settings
2075
+ // write, a saved view): the mode stays what the last list said.
2076
+
2077
+
2078
+ if (tree) this.treeMode = tree.mode ?? null;else if (Array.isArray(data.records)) this.treeMode = null;
2079
+
2080
+ if (this.treeMode === "tree" && Array.isArray(data.records)) {
2081
+ const reopen = [...this.tree.expanded];
2082
+ (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.acceptLevel)(this.tree, {
2083
+ records: copy(data.records),
2084
+ hasNext: tree.has_next === true,
2085
+ cursor: tree.cursor ?? null,
2086
+ append: this.continuing && this.records.length > 0
2087
+ });
2088
+ this.records = (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.knownRecords)(this.tree);
2089
+ this.appended = null; // Branches a person had open stay open: their children are asked
2090
+ // for again, since a re-read may have changed them.
2091
+
2092
+ for (const id of reopen) {
2093
+ if (this.tree.nodes.get(id)?.has_children === true && !(0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.hasLevel)(this.tree, id)) this.expandNode(id);
2094
+ }
2095
+ } else if (Array.isArray(data.records)) {
2096
+ const next = copy(data.records); // A continuation keeps what is already shown and adds to it, and
2097
+ // keeps it by identity: a record the port repeats is not duplicated.
2098
+
2099
+ if (this.continuing && this.records.length) {
2100
+ const known = new Set(this.records.map(record => record.id));
2101
+ this.appended = next.filter(record => !known.has(record.id));
2102
+ this.records = [...this.records, ...this.appended];
2103
+ } else {
2104
+ this.appended = null;
2105
+ this.records = next; // A new result set: the table draws it as new rows where a
2106
+ // kept one would move (sf-view-table setRows "result"). A
2107
+ // revalidation of the printed answer is the same result.
2108
+
2109
+ this._newResult = !inPlace;
2110
+ }
2111
+ } // The same columns are not given again: the table would take them as
2112
+ // new and measure its widths again.
2113
+
2114
+
2115
+ if (Array.isArray(data.columns) && !(inPlace && canonical(data.columns) === this._columnsKey)) {
2116
+ this.columns = data.columns.map(projectColumn);
2117
+ this._columnsKey = canonical(data.columns);
2118
+ }
2119
+
2120
+ if (Number.isSafeInteger(data.total) && data.total >= 0) {
2121
+ this.total = data.total;
2122
+ this.countKnown = true;
2123
+ } else if (typeof data.has_next === "boolean") {
2124
+ this.hasNext = data.has_next; // A number already answered for this very query stays on screen.
2125
+ // Paging and Show more do not take it away — only a query that
2126
+ // changes what is being counted does.
2127
+
2128
+ this.countKnown = this.countedQuery !== null && this.countedQuery === this._countKey();
2129
+ }
2130
+
2131
+ if (Number.isSafeInteger(data.total)) this.totalExact = data.total_exact !== false;
2132
+ this._answered = true;
2133
+ if (Array.isArray(data.saved_views)) this.savedViews = copy(data.saved_views);
2134
+ if (data.column_settings) this.columnSettings = copy(data.column_settings);
2135
+
2136
+ if (data.echo) {
2137
+ const echo = data.echo;
2138
+ this.query = { ...this.query,
2139
+ filters: copy(echo.filters_chosen ?? echo.filters ?? {}),
2140
+ search: echo.search ?? "",
2141
+ sort: copy(echo.sort ?? []),
2142
+ page: echo.page ?? 1,
2143
+ per_page: echo.per_page ?? 25
2144
+ };
2145
+ } // The search box says what the list is searched for, whoever set it: a
2146
+ // changeQuery({search}) from page code filtered the rows and left the
2147
+ // box empty (docs trial 5). Written silently -- it is not a new search.
2148
+
2149
+
2150
+ this._syncSearchBoxes(this.query.search ?? "");
2151
+
2152
+ this._renderProjection();
2153
+
2154
+ this._restoreOpenedView(); // The empty result is shown by the table, in its own body. The view only
2155
+ // announces it, so a search that finds nothing moves nothing.
2156
+
2157
+
2158
+ this._setState(this.records.length ? "populated" : "empty", this.records.length ? "" : (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "emptyText", this));
2159
+
2160
+ this.notify("");
2161
+ } // What a count belongs to. Search, filters and page size change what the
2162
+ // number would be; a page does not.
2163
+
2164
+
2165
+ _countKey() {
2166
+ return JSON.stringify([this.query.search ?? "", this.query.filters ?? {}, this.query.per_page ?? 25]);
2167
+ } // What the band shows and what it does. The words and the action list stay
2168
+ // the host's, set on the pagination as they always were; the band only reads
2169
+ // them, so a host configures one element and sees the band in the card.
2170
+
2171
+
2172
+ _renderBulkBand() {
2173
+ const pagination = this.pagination;
2174
+ const table = this.table;
2175
+ const selected = table?.getSelectedRecordIds?.() ?? [];
2176
+ const actions = pagination?.actions ?? [];
2177
+
2178
+ if (!pagination || !selected.length || !actions.length) {
2179
+ if (this._bulkBand?.isConnected) this._bulkBand.remove();
2180
+ this._bulkBand = null;
2181
+ return this;
2182
+ }
2183
+
2184
+ const band = this.bulkBand;
2185
+ band.setAttribute("aria-label", pagination.actionsRegionLabel ?? "");
2186
+ const action = pagination.action ?? "";
2187
+ const total = pagination.countKnown === false ? null : pagination.total;
2188
+ const forAllLabel = total == null ? pagination.actionForAllLabel : `${pagination.actionForAllLabel} (${this._shownTotal(total)})`;
2189
+
2190
+ const chosen = (node, name, value) => {
2191
+ if (value == null || value === false) node.removeAttribute(name);else node.setAttribute(name, value === true ? "" : String(value));
2192
+ };
2193
+
2194
+ const parts = {};
2195
+
2196
+ if (!band.childElementCount) {
2197
+ band.innerHTML = `
2198
+ <span class="sf-pagination-actions-count" data-sf-bulk="count"></span>
2199
+ <span class="sf-pagination-actions-separator" aria-hidden="true"></span>
2200
+ <sf-dropdown data-sf-bulk="actions" size="1" appearance="outlined" search="false" mode="select"></sf-dropdown>
2201
+ <sf-button data-sf-bulk="apply" size="1" appearance="default" scheme="primary"></sf-button>
2202
+ <span class="sf-pagination-actions-separator" aria-hidden="true" data-sf-bulk="for-all-separator"></span>
2203
+ <sf-checkbox data-sf-bulk="for-all"></sf-checkbox>
2204
+ <sf-button-icon data-sf-bulk="clear" class="sf-pagination-actions-clear" size="1" appearance="link" scheme="on-surface" icon="close"></sf-button-icon>`;
2205
+ band.querySelector('[data-sf-bulk="apply"]').addEventListener("click", () => this.applyBulkAction());
2206
+ band.querySelector('[data-sf-bulk="actions"]').addEventListener("change", event => pagination.setAction?.(event.detail?.value ?? ""));
2207
+ band.querySelector('[data-sf-bulk="for-all"]').addEventListener("change", event => pagination.setActionForAll?.(Boolean(event.target?.checked)));
2208
+ band.querySelector('[data-sf-bulk="clear"]').addEventListener("click", () => this.clearSelection());
2209
+ }
2210
+
2211
+ for (const node of band.querySelectorAll("[data-sf-bulk]")) {
2212
+ parts[node.dataset.sfBulk] = node;
2213
+ if (node.localName.startsWith("sf-")) chosen(node, "size", this.size);
2214
+ }
2215
+
2216
+ parts.count.textContent = `${pagination.selectedLabel} ${selected.length}`;
2217
+ chosen(parts.actions, "value", action);
2218
+ chosen(parts.actions, "placeholder", pagination.actionChooseLabel);
2219
+ chosen(parts.actions, "aria-label", pagination.actionsLabel);
2220
+ parts.actions.options = [{
2221
+ type: "text",
2222
+ size: this.size,
2223
+ text: pagination.actionChooseLabel,
2224
+ value: "",
2225
+ selected: !action
2226
+ }, ...actions.map(item => ({
2227
+ type: "text",
2228
+ size: this.size,
2229
+ text: item.text,
2230
+ value: item.value,
2231
+ selected: item.value === action,
2232
+ disabled: Boolean(item.disabled)
2233
+ }))];
2234
+ chosen(parts.apply, "text", pagination.actionApplyText);
2235
+ chosen(parts.apply, "disabled", !action);
2236
+ chosen(parts["for-all"], "label", forAllLabel);
2237
+ chosen(parts["for-all"], "checked", Boolean(pagination.actionForAll));
2238
+ parts["for-all"].hidden = !pagination.showActionForAll; // Its separator goes with it, or the band ends on a rule before the
2239
+ // close button.
2240
+
2241
+ parts["for-all-separator"].hidden = !pagination.showActionForAll;
2242
+ chosen(parts.clear, "title", pagination.clearSelectionLabel);
2243
+ chosen(parts.clear, "aria-label", pagination.clearSelectionLabel);
2244
+ return this;
2245
+ } // Apply is the band's only write, and it goes the same way it went from the
2246
+ // page row: under the filter when For-all is ticked, over the marked rows
2247
+ // otherwise.
2248
+
2249
+
2250
+ applyBulkAction() {
2251
+ const pagination = this.pagination;
2252
+ const action = pagination?.action;
2253
+ if (!action) return undefined;
2254
+ if (pagination.actionForAll) return this.applyUnderFilter(action, this.total);
2255
+ return this._action({
2256
+ action_id: action,
2257
+ record_ids: this.table?.getSelectedRecordIds?.() ?? []
2258
+ }, {
2259
+ selectedBulk: true
2260
+ });
2261
+ }
2262
+
2263
+ clearSelection() {
2264
+ this.table?.clearSelection?.();
2265
+ this.pagination?.setState?.({
2266
+ selectedCount: 0
2267
+ });
2268
+ return this._renderBulkBand();
2269
+ } // One record, changed in place. A write that touches a row does not need
2270
+ // the page re-queried and the body rebuilt: the host answers with the
2271
+ // record it changed, or the page calls this itself, and only that row is
2272
+ // written. A full projection is still a full projection.
2273
+
2274
+
2275
+ updateRecord(id, patch = {}) {
2276
+ const key = String(id);
2277
+
2278
+ if (this.treeMode === "tree") {
2279
+ const known = this.tree.nodes.get(key);
2280
+ if (!known || !patch || typeof patch !== "object") return false;
2281
+ this.tree.nodes.set(key, { ...known,
2282
+ ...copy(patch),
2283
+ id: known.id
2284
+ });
2285
+ this.records = (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.knownRecords)(this.tree);
2286
+ this.table?.setRows?.((0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.visibleRows)(this.tree));
2287
+
2288
+ this._renderBulkBand();
2289
+
2290
+ return true;
2291
+ }
2292
+
2293
+ const index = this.records.findIndex(record => String(record?.id) === key);
2294
+ if (index < 0 || !patch || typeof patch !== "object") return false;
2295
+ const next = { ...this.records[index],
2296
+ ...copy(patch),
2297
+ id: this.records[index].id
2298
+ };
2299
+ this.records = [...this.records.slice(0, index), next, ...this.records.slice(index + 1)];
2300
+ this.table?.updateRowByKey?.("id", this.records[index].id, { ...next
2301
+ }, "record-update");
2302
+
2303
+ this._renderBulkBand();
2304
+
2305
+ return true;
2306
+ } // ─── the tree (port 1.8.0) ───────────────────────────────────────────────
2307
+ // Opening a node shows the children the view already has, or asks the host
2308
+ // for them; closing it is the view's own business. A node is opened only
2309
+ // when the host said it has children.
2310
+
2311
+
2312
+ expandNode(id) {
2313
+ const key = String(id);
2314
+ const node = this.tree.nodes.get(key);
2315
+ if (!node || node.has_children !== true) return null;
2316
+
2317
+ if ((0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.hasLevel)(this.tree, key)) {
2318
+ (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.expandKnown)(this.tree, key);
2319
+
2320
+ this._renderTreeRows();
2321
+
2322
+ return null;
2323
+ }
2324
+
2325
+ (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.markLoading)(this.tree, key);
2326
+
2327
+ this._renderTreeRows();
2328
+
2329
+ return this._raise("tree.expand", {
2330
+ id: node.id,
2331
+ cursor: null,
2332
+ sequence: ++this.treeSequence
2333
+ });
2334
+ }
2335
+
2336
+ collapseNode(id) {
2337
+ (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.collapse)(this.tree, id);
2338
+
2339
+ this._renderTreeRows();
2340
+ } // The next page of one level: of the roots through the query, as a list
2341
+ // pages, and of a node's children through tree.expand with its cursor.
2342
+
2343
+
2344
+ loadMoreNodes(parent, cursor) {
2345
+ if (parent === null || parent === undefined || parent === "") {
2346
+ return this.changeQuery({
2347
+ page: this.query.page + 1
2348
+ }, "show-more");
2349
+ }
2350
+
2351
+ const key = String(parent);
2352
+
2353
+ this._treeAppend.add(key);
2354
+
2355
+ const node = this.tree.nodes.get(key);
2356
+ return this._raise("tree.expand", {
2357
+ id: node?.id ?? parent,
2358
+ cursor: cursor ?? null,
2359
+ sequence: ++this.treeSequence
2360
+ });
2361
+ } // Where a node may go is the host's: it is asked first, and only the places
2362
+ // it names are offered -- the top level when it allows that, and the first
2363
+ // or last place among the new siblings when it allows reordering.
2364
+
2365
+
2366
+ async openMoveDialog(record) {
2367
+ const answer = await this._raise("node.targets", {
2368
+ id: record.id
2369
+ });
2370
+ if (answer?.answer !== "applied") return answer;
2371
+ const targets = answer.data?.move_targets ?? {};
2372
+ const parents = Array.isArray(targets.parents) ? targets.parents : [];
2373
+ const options = [...(targets.root ? [{
2374
+ id: null,
2375
+ label: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "moveRoot", this),
2376
+ path: []
2377
+ }] : []), ...parents.filter(parent => String(parent.id) !== String(record.id))];
2378
+
2379
+ if (!options.length) {
2380
+ this.notify((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "moveNoTargets", this), {
2381
+ kind: "warning"
2382
+ });
2383
+ return {
2384
+ answer: "refused",
2385
+ reason: "no-targets"
2386
+ };
2387
+ }
2388
+
2389
+ const label = String(record.title ?? record.label ?? record.name ?? record.id); // The choices are sf-radio, which nothing else on the page may have
2390
+ // asked for: a Loader that scans the light DOM never meets the
2391
+ // dialog's (Larena: an empty choice list, 2026-10-12). _dialog asks
2392
+ // for it with the window, and waits briefly.
2393
+
2394
+ const chosen = await this._dialog({
2395
+ title: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "moveTitle", this).replace("{label}", label),
2396
+ apply: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "moveApply", this),
2397
+ options,
2398
+ reorder: targets.reorder === true
2399
+ });
2400
+ if (!chosen) return null;
2401
+ return this._raise("node.move", {
2402
+ id: record.id,
2403
+ parent: chosen.parent,
2404
+ index: chosen.index,
2405
+ ...(record.revision !== undefined ? {
2406
+ revision: record.revision
2407
+ } : {})
2408
+ });
2409
+ } // The elements the view composes, asked for by tag: the Loader defines each
2410
+ // from its own bundle (the rule's relations usually have already), and the
2411
+ // view projects again once they are, so an answer that came before them is
2412
+ // not lost on an element that could not take it yet.
2413
+
2414
+
2415
+ _askForParts(tags) {
2416
+ const registry = globalThis.customElements;
2417
+ if (typeof registry?.get !== "function" || typeof registry.whenDefined !== "function") return;
2418
+ const missing = tags.filter(tag => !registry.get(tag));
2419
+ if (!missing.length) return;
2420
+ const key = missing.join(",");
2421
+ this._partsAsked ??= new Set();
2422
+ if (this._partsAsked.has(key)) return;
2423
+
2424
+ this._partsAsked.add(key);
2425
+
2426
+ globalThis.SF?.Loader?.ensure?.(missing, {
2427
+ timeout: Infinity
2428
+ }).then(() => {
2429
+ this._partsAsked.delete(key);
2430
+
2431
+ if (this.isConnected && missing.some(tag => tag !== "sf-toast")) this._renderProjection();
2432
+ });
2433
+ }
2434
+
2435
+ async _ensureElements(tags) {
2436
+ return globalThis.SF?.Loader?.ensure?.(tags);
2437
+ } // One small window for both the move and the confirmation: the
2438
+ // Framework's sf-modal, a modal layer of the one document stack (core
2439
+ // layer-stack.js) with the shared scrim, Escape, a press on the scrim and
2440
+ // the focus kept inside and given back. It was a native <dialog> shown with
2441
+ // showModal(), outside the stack and over the browser's own backdrop, so a
2442
+ // drawer or a modal opened from it, or under it, did not know it was there
2443
+ // (reuse audit 2026-11-02, findings 1 and 2). The window stands in this
2444
+ // element (slot dialog) and draws in its portal at the end of the body, in
2445
+ // this element's language and theme. Resolves with what was chosen, true
2446
+ // for a plain confirmation, or null.
2447
+
2448
+
2449
+ async _dialog({
2450
+ title,
2451
+ text = "",
2452
+ apply,
2453
+ options = null,
2454
+ reorder = false
2455
+ }) {
2456
+ await this._ensureElements(["sf-modal", "sf-button", ...(options ? ["sf-radio"] : [])]);
2457
+
2458
+ if (!globalThis.customElements?.get("sf-modal")) {
2459
+ this.notify((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "noticeUnavailable", this));
2460
+ return null;
2461
+ }
2462
+
2463
+ const modal = document.createElement("sf-modal");
2464
+ modal.slot = "dialog";
2465
+ modal.className = "sf-data-view-dialog";
2466
+ modal.setAttribute("heading", title);
2467
+ modal.setAttribute("aria-label", title);
2468
+ modal.setAttribute("width", "min(var(--sf-g4), calc(100vw - 2 * var(--sf-space-3)))"); // The places can be many; the list scrolls and the title and the
2469
+ // buttons stay where they are.
2470
+
2471
+ const content = document.createElement("div");
2472
+ content.slot = "content";
2473
+ content.className = "flex flex-col gap-3";
2474
+
2475
+ if (text) {
2476
+ const body = document.createElement("p");
2477
+ body.className = "sf-text-1 m-0";
2478
+ body.textContent = text;
2479
+ content.append(body);
2480
+ }
2481
+
2482
+ const name = `${this.instanceId}-move`;
2483
+
2484
+ if (options) {
2485
+ const list = document.createElement("div");
2486
+ list.setAttribute("role", "radiogroup");
2487
+ list.setAttribute("aria-label", title);
2488
+ list.className = "flex flex-col gap-1";
2489
+ list.style.cssText = "overflow-y: auto; min-block-size: 0; flex: 1 1 auto;";
2490
+ options.forEach((option, index) => {
2491
+ const radio = document.createElement("sf-radio");
2492
+ radio.setAttribute("name", name);
2493
+ radio.setAttribute("value", option.id === null ? "" : String(option.id));
2494
+ radio.setAttribute("label", String(option.label));
2495
+ if (Array.isArray(option.path) && option.path.length) radio.setAttribute("description", option.path.join(" / "));
2496
+ if (index === 0) radio.setAttribute("checked", "");
2497
+ list.append(radio);
2498
+ });
2499
+ content.append(list);
2500
+
2501
+ if (reorder) {
2502
+ const placeList = document.createElement("div");
2503
+ placeList.setAttribute("role", "radiogroup");
2504
+ placeList.setAttribute("aria-label", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "movePlace", this));
2505
+ placeList.className = "flex gap-2";
2506
+
2507
+ for (const [value, key] of [["last", "moveLast"], ["first", "moveFirst"]]) {
2508
+ const radio = document.createElement("sf-radio");
2509
+ radio.setAttribute("name", `${name}-place`);
2510
+ radio.setAttribute("value", value);
2511
+ radio.setAttribute("label", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, key, this));
2512
+ if (value === "last") radio.setAttribute("checked", "");
2513
+ placeList.append(radio);
2514
+ }
2515
+
2516
+ content.append(placeList);
2517
+ }
2518
+ }
2519
+
2520
+ const footer = document.createElement("div");
2521
+ footer.slot = "footer";
2522
+ footer.className = "flex justify-end gap-2";
2523
+ const cancel = document.createElement("sf-button");
2524
+ cancel.setAttribute("appearance", "outline");
2525
+ cancel.setAttribute("text", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "cancel", this));
2526
+ const confirm = document.createElement("sf-button");
2527
+ confirm.setAttribute("text", apply);
2528
+ confirm.dataset.apply = "";
2529
+ footer.append(cancel, confirm);
2530
+ modal.append(content, footer);
2531
+ this.append(modal); // The nodes are the page's own, drawn in the window's portal: they are
2532
+ // read where they are.
2533
+
2534
+ const checked = radioName => [...content.querySelectorAll(`sf-radio[name="${radioName}"]`)].find(radio => radio.checked === true || radio.hasAttribute("checked") && radio.checked !== false);
2535
+
2536
+ let chosen = null;
2537
+ cancel.addEventListener("click", () => modal.close({
2538
+ reason: "cancelled"
2539
+ }));
2540
+ confirm.addEventListener("click", () => {
2541
+ if (!options) {
2542
+ chosen = true;
2543
+ } else {
2544
+ const parentRadio = checked(name);
2545
+ const value = parentRadio?.getAttribute("value") ?? "";
2546
+ const option = options.find(item => (item.id === null ? "" : String(item.id)) === value) ?? options[0];
2547
+ const place = reorder ? checked(`${name}-place`)?.getAttribute("value") : "last";
2548
+ chosen = {
2549
+ parent: option.id,
2550
+ index: place === "first" ? 0 : null
2551
+ };
2552
+ }
2553
+
2554
+ modal.confirm();
2555
+ }); // Asked with a signal, open() answers when the window closes (§19.4):
2556
+ // accepted through confirm(), else closed by Escape, the scrim, the
2557
+ // close button or «Отмена».
2558
+
2559
+ const scope = new AbortController();
2560
+ const answer = await modal.open({}, {
2561
+ signal: scope.signal
2562
+ });
2563
+ modal.remove();
2564
+ return answer?.outcome === "applied" && answer.value?.accepted === true ? chosen : null;
2565
+ }
2566
+
2567
+ _renderTreeRows() {
2568
+ this.records = (0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.knownRecords)(this.tree);
2569
+ this.table?.setRows?.((0,_js_tree_js__WEBPACK_IMPORTED_MODULE_11__.visibleRows)(this.tree));
2570
+ } // An answer that carries records and nothing else is a patch of those
2571
+ // records, not a new page: it has no echo of the query, no total and no
2572
+ // columns, so there is nothing to lay out again.
2573
+
2574
+
2575
+ _isRecordPatch(data) {
2576
+ return Boolean(data) && Array.isArray(data.records) && !data.echo && data.total == null && !data.columns && !data.saved_views && !data.column_settings && !data.tree;
2577
+ } // A checkable cell (record.set, capability inline-edit): the host answers
2578
+ // with the record as it now is, asks to confirm first (a change that
2579
+ // rewrites records), or refuses; the row is then drawn from what it said.
2580
+
2581
+
2582
+ async setCell({
2583
+ id,
2584
+ key,
2585
+ value,
2586
+ revision
2587
+ } = {}) {
2588
+ if (id == null || !key) return null;
2589
+ const record = this.records.find(entry => String(entry.id) === String(id));
2590
+ const answer = await this._raise("record.set", {
2591
+ record_id: id,
2592
+ key,
2593
+ value,
2594
+ revision: revision ?? record?._revision ?? record?.revision ?? null
2595
+ });
2596
+ this.table?.clearCellPending?.(String(id), key); // Anything but applied leaves the record as it was, and the box shows
2597
+ // the value the host still holds.
2598
+
2599
+ if (answer?.answer !== "applied") {
2600
+ const held = this.records.find(entry => String(entry.id) === String(id));
2601
+ this.table?.resetCellControl?.(String(id), key, held?.[key]);
2602
+ }
2603
+
2604
+ return answer;
2605
+ } // Rows dragged into another order (record.reorder, capability
2606
+ // row-reorder). Shown in the new order at once; a refusal or a conflict
2607
+ // puts the old order back.
2608
+
2609
+
2610
+ async reorderRows({
2611
+ ids = [],
2612
+ before = null,
2613
+ order = null
2614
+ } = {}) {
2615
+ if (!ids.length) return null;
2616
+ const previous = this.records;
2617
+
2618
+ if (Array.isArray(order)) {
2619
+ const byId = new Map(this.records.map(record => [String(record.id), record]));
2620
+ const placed = order.map(key => byId.get(String(key))).filter(Boolean);
2621
+
2622
+ if (placed.length === this.records.length) {
2623
+ this.records = placed;
2624
+
2625
+ this._renderProjection();
2626
+ }
2627
+ }
2628
+
2629
+ const first = previous.find(record => ids.map(String).includes(String(record.id)));
2630
+ const answer = await this._raise("record.reorder", {
2631
+ record_ids: [...ids],
2632
+ before,
2633
+ revision: first?._revision ?? first?.revision ?? null
2634
+ });
2635
+
2636
+ if (answer?.answer !== "applied") {
2637
+ this.records = previous;
2638
+
2639
+ this._renderProjection();
2640
+ }
2641
+
2642
+ return answer;
2643
+ }
2644
+
2645
+ _applyRecordPatch(records = []) {
2646
+ let applied = 0;
2647
+
2648
+ for (const record of records) {
2649
+ if (record && record.id != null && this.updateRecord(record.id, record)) applied += 1;
2650
+ } // A record the page does not hold cannot be patched into it; that is a
2651
+ // new row, and only a query knows where it belongs.
2652
+
2653
+
2654
+ if (applied !== records.length) this.refresh("record-unknown");else this.notify("");
2655
+ return applied;
2656
+ } // The catalogue of row actions the table names in its own constructor --
2657
+ // the eye for «view», the pencil for «edit». It is captured once and kept,
2658
+ // because the projection writes its own narrowed list onto the very same
2659
+ // property: reading that list back would make the narrowed list the
2660
+ // catalogue, which is what the single read guarded against.
2661
+ //
2662
+ // The capture used to be one `??=` read of the table's own property with an
2663
+ // empty list behind it, which is where the defect was.
2664
+ // A list the view draws itself stands in the shadow template, so it is in
2665
+ // the tree before sf-view-table is defined, and an element that has not upgraded
2666
+ // carries no ACTIONS_SETTINGS_ROW at all: `[]` was cached, and `??=` never
2667
+ // looked again, because `[]` is not nullish. From then on every id was
2668
+ // unknown -- the warning below fired and «view» came out as a generic word
2669
+ // instead of the eye, on every list (Larena storage QA, 2026-10-22). So the
2670
+ // capture looks again while it holds nothing, and the list this view wrote
2671
+ // is remembered so it is never taken for the table's own.
2672
+
2673
+
2674
+ _captureRowActions(table) {
2675
+ if (this._knownRowActions?.length) return this._knownRowActions;
2676
+ const own = table?.ACTIONS_SETTINGS_ROW;
2677
+
2678
+ const named = Array.isArray(own) && own.length > 0 && own !== this._projectedRowActions;
2679
+
2680
+ this._knownRowActions = named ? own : [];
2681
+ if (!named) this._awaitRowActions();
2682
+ return this._knownRowActions;
2683
+ } // Waited for once, and the projection drawn again when the definition
2684
+ // lands: on a page whose list has settled nothing else would make the view
2685
+ // draw, and the catalogue can only be read from an upgraded element. The
2686
+ // Loader is told which tag is wanted, as it is for the move dialog's own
2687
+ // elements.
2688
+
2689
+
2690
+ _awaitRowActions() {
2691
+ if (this._awaitingRowActions) return;
2692
+ this._awaitingRowActions = true;
2693
+ globalThis.SF?.Loader?.ensure?.(["sf-view-table"]); // Read off globalThis and asked for softly: a stand that renders this
2694
+ // view over a document of its own need not keep a registry, and a view
2695
+ // that cannot wait simply keeps the fallback words.
2696
+
2697
+ const defined = globalThis.customElements?.whenDefined?.("sf-view-table");
2698
+ defined?.then?.(() => {
2699
+ if (this.isConnected) this._renderProjection();
2700
+ });
2701
+ }
2702
+
2703
+ _renderProjection() {
2704
+ this._capabilitiesKey = capabilitiesKey(this.knownPort);
2705
+
2706
+ this._readPrintedPageSize();
2707
+
2708
+ this._applyForAll();
2709
+
2710
+ if (this._current !== undefined && this.table && this.table.current !== this._current) this.table.current = this._current; // The saved-view controls appear when the host keeps saved views and
2711
+ // not before: this is the only place that runs both when a host is set
2712
+ // and when data arrives, which is when the answer can have changed.
2713
+
2714
+ const savedViews = this.shadowRoot?.querySelector?.("[data-saved-views]");
2715
+
2716
+ if (savedViews) {
2717
+ savedViews.hidden = !this.knownCapability("saved-views");
2718
+ }
2719
+
2720
+ this.pagination?.setState?.({
2721
+ showMoreBusy: false
2722
+ }); // The page row no longer carries the band: the card does, so the strip
2723
+ // can hold the bottom of the window without the row coming along. The
2724
+ // attribute is written rather than put through setState, which clears a
2725
+ // false value by removing the attribute — and a removed `bottom` means
2726
+ // the default, which is to draw the band.
2727
+
2728
+ this.pagination?.setAttribute?.("bottom", "false");
2729
+ this.pagination?.setAttribute?.("size", this.size);
2730
+
2731
+ this._forwardLabels();
2732
+
2733
+ const table = this.table;
2734
+
2735
+ if (table) {
2736
+ // The two chrome columns. Written as attributes rather than left to the
2737
+ // table's own defaults, which are both on: an embedded list rendered a
2738
+ // checkbox column with «select all rows» and a gear column, and both
2739
+ // read as leftovers from a page table -- there is no bulk band and no
2740
+ // toolbar in an embedded list for either of them to lead to.
2741
+ // A refused list offers nothing to do with it: no selection, no
2742
+ // «Создать», no search or filter over rows the person may not see
2743
+ // (docs trial 5: they stayed active under «no access»). The
2744
+ // author's own toolbar setting comes back with the access.
2745
+ const refused = this._queryRefused === true;
2746
+ if (!table.hasAttribute?.("data-sf-host-toolbar")) table.setAttribute("data-sf-host-toolbar", table.getAttribute?.("toolbar") ?? "");
2747
+ const authorToolbar = table.getAttribute?.("data-sf-host-toolbar");
2748
+ if (refused) table.setAttribute("toolbar", "false");else if (authorToolbar) table.setAttribute("toolbar", authorToolbar);else table.removeAttribute?.("toolbar"); // The row boxes and the gear column are the view's chrome, unless the
2749
+ // host wrote them off on its table: what the table carried before the
2750
+ // view first touched it is the host's word, as for row-actions below.
2751
+ // A table written selectable="false" came back selectable="true" and
2752
+ // drew a box in every row of a list that has no bulk actions (Larena,
2753
+ // the sites list, 2026-11-05).
2754
+
2755
+ table.setAttribute("selectable", this.renders("select") && !refused && !hostWroteOff(table, "selectable") ? "true" : "false"); // A page's list becomes the compact list on a narrow container; an
2756
+ // embedded one (no toolbar) stays a table, and a layout the host
2757
+ // wrote on its table is the host's.
2758
+
2759
+ if (!table.hasAttribute?.("data-sf-host-layout")) {
2760
+ table.setAttribute("data-sf-host-layout", table.getAttribute?.("layout") ?? "");
2761
+ }
2762
+
2763
+ if (!table.getAttribute?.("data-sf-host-layout")) {
2764
+ table.setAttribute("layout", this.renders("toolbar") ? "auto" : "table");
2765
+ }
2766
+
2767
+ table.setAttribute("size", this.size);
2768
+
2769
+ this._tableScrollMode(table);
2770
+
2771
+ table.setAttribute("settings", this.renders("settings") && !hostWroteOff(table, "settings") ? "true" : "false"); // The column settings, without the column: it also carries each
2772
+ // row's menu, which is the host's row-actions, not this.
2773
+
2774
+ table.setAttribute("settings-menu", this.hostOffers("personal-column-settings") ? "true" : "false");
2775
+ table.setAttribute("creatable", this.hostOffers("create-record") && !refused ? "true" : "false");
2776
+ table.setAttribute("row-reorder", this.hostOffers("row-reorder") && this.treeMode !== "tree" ? "true" : "false");
2777
+ table.setAttribute("inline-edit", this.hostOffers("inline-edit") ? "true" : "false"); // A filter template is a saved view the table keeps by itself; in a
2778
+ // view it went nowhere, so it is offered where the host keeps views.
2779
+
2780
+ table.setAttribute("filter-templates", this.hostOffers("saved-views") ? "true" : "false"); // Row actions are the host's to claim. The intent was already
2781
+ // refused without the capability -- answered «unavailable» without
2782
+ // the host being called -- but the column and its buttons were drawn
2783
+ // all the same, so a person could press an action nothing would ever
2784
+ // apply. A view does not show a control the host has not claimed,
2785
+ // which is the same rule the saved-view controls already follow
2786
+ // (admin polygon, 2026-10-10).
2787
+ // A table the host wrote row-actions="false" on keeps it: the host has
2788
+ // put every action in its own row menu and still claims the
2789
+ // capability, because its menu raises record.open and record.mutate
2790
+ // through the port (Larena, 2026-10-12). What the table carried
2791
+ // before the view first touched it is the host's word.
2792
+
2793
+ if (!table.hasAttribute?.("data-sf-host-row-actions")) {
2794
+ table.setAttribute("data-sf-host-row-actions", table.getAttribute?.("row-actions") ?? "");
2795
+ }
2796
+
2797
+ const hostSaysNo = table.getAttribute?.("data-sf-host-row-actions") === "false";
2798
+ table.setAttribute("row-actions", !hostSaysNo && this.knownCapability("row-actions") ? "true" : "false"); // The accessible name of a list the host did not slot in. The fallback
2799
+ // table carries a name of its own, which is right for a page list and
2800
+ // wrong inside a group called «Счета»: it would announce «Записи».
2801
+ // Only the fallback is renamed -- a table a host slotted in keeps the
2802
+ // name the host gave it.
2803
+
2804
+ const slot = this.shadowRoot.querySelector('slot[name="content"]');
2805
+ const own = this.getAttribute("aria-label");
2806
+ if (own && slot && !slot.assignedElements().length) table.setAttribute("aria-label", own);
2807
+ const allowed = new Set(this.records.flatMap(record => Array.isArray(record.actions) ? record.actions : [])); // The catalogue is read once and kept, from an element that has
2808
+ // upgraded: narrowing the table's own list in place is destructive,
2809
+ // and so is capturing from an element that has nothing to read yet.
2810
+ // Both left the catalogue empty, and from then on every id the host
2811
+ // named was unknown -- «view» came out as a link labelled "view"
2812
+ // instead of the eye, and in a narrow column the label broke
2813
+ // mid-word and set the row height. _captureRowActions above.
2814
+
2815
+ const known = this._captureRowActions(table); // The host's own actions are named by its catalogue (row_actions,
2816
+ // port 1.8.0); an action the catalogue groups is offered once, as
2817
+ // the group, with a submenu of the members a record allows. Which
2818
+ // members a row shows is still that record's actions alone.
2819
+
2820
+
2821
+ const catalog = new Map(this.rowActionCatalog.map(entry => [entry.id, entry])); // In a tree, move and create are the view's own actions and named
2822
+ // by it, unless the host's catalogue names them.
2823
+
2824
+ const treeActions = this.treeMode === "tree" ? {
2825
+ move: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "moveAction", this),
2826
+ ...(this.hostOffers("create-record") ? {
2827
+ create: (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "createChildAction", this)
2828
+ } : {})
2829
+ } : {};
2830
+
2831
+ const single = id => {
2832
+ const entry = catalog.get(id) ?? (treeActions[id] ? {
2833
+ id,
2834
+ label: treeActions[id]
2835
+ } : null);
2836
+
2837
+ if (entry) {
2838
+ return {
2839
+ id,
2840
+ name: entry.label,
2841
+ danger: entry.danger === true,
2842
+ inline: false,
2843
+ component: {
2844
+ type: "button",
2845
+ props: {
2846
+ text: entry.label,
2847
+ appearance: "link",
2848
+ ...(entry.icon ? {
2849
+ icon: entry.icon
2850
+ } : {})
2851
+ }
2852
+ }
2853
+ };
2854
+ } // An id the host's catalogue does not name gets a word, not its
2855
+ // raw id clipped in the column, and the host is told (docs trial 4).
2856
+
2857
+
2858
+ const found = known.find(action => action.id === id);
2859
+ if (found) return found;
2860
+ console.warn(`[sf-view] row action «${id}» has no row_actions entry; name it in the host's catalogue.`);
2861
+ const label = (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "unnamedAction", this);
2862
+ return {
2863
+ id,
2864
+ name: label,
2865
+ inline: false,
2866
+ component: {
2867
+ type: "button",
2868
+ props: {
2869
+ text: label,
2870
+ appearance: "link",
2871
+ title: id
2872
+ }
2873
+ }
2874
+ };
2875
+ };
2876
+
2877
+ const groups = new Map();
2878
+ const listed = [];
2879
+
2880
+ for (const id of allowed) {
2881
+ const group = catalog.get(id)?.group;
2882
+
2883
+ if (!group) {
2884
+ listed.push(single(id));
2885
+ continue;
2886
+ }
2887
+
2888
+ if (!groups.has(group)) {
2889
+ const label = catalog.get(id).group_label ?? group;
2890
+ const entry = {
2891
+ id: `group:${group}`,
2892
+ name: label,
2893
+ members: [],
2894
+ inline: false,
2895
+ component: {
2896
+ type: "button",
2897
+ props: {
2898
+ text: label,
2899
+ appearance: "link"
2900
+ }
2901
+ }
2902
+ };
2903
+ groups.set(group, entry);
2904
+ listed.push(entry);
2905
+ }
2906
+
2907
+ groups.get(group).members.push(single(id));
2908
+ }
2909
+
2910
+ table.ACTIONS_SETTINGS_ROW = listed; // Remembered by identity, so the capture never reads this narrowed
2911
+ // list back as the table's own catalogue.
2912
+
2913
+ this._projectedRowActions = listed; // A list whose every action lives in the row menu has no actions
2914
+ // column to draw.
2915
+
2916
+ if (listed.length && listed.every(action => action.inline === false)) {
2917
+ table.setAttribute("row-actions", "false");
2918
+ } // The port the list is holding, handed to the table for one thing:
2919
+ // the renderer set asks a host how to format a value, and a cell had
2920
+ // no host to ask. Only the formatting is read from it -- the table
2921
+ // raises its intents as it always has and the view re-raises them.
2922
+
2923
+
2924
+ table.cellHost = this.hostPort ?? null;
2925
+ table.setColumns?.(this.columns);
2926
+
2927
+ if (Array.isArray(this.appended)) {
2928
+ // Appending leaves the rows already on screen untouched, so a
2929
+ // row that was checked stays checked: the checkbox state lives
2930
+ // on the row itself.
2931
+ table.addRows?.(this.appended);
2932
+ } else {
2933
+ table.setRows?.(this.cappedRecords, this._newResult ? "result" : "rows");
2934
+ this._newResult = false;
2935
+ }
2936
+
2937
+ const first = this.query.sort[0];
2938
+ table.setSort?.(first?.key ?? null, first?.direction ?? null);
2939
+ table.setTemplates?.(Object.fromEntries(this.savedViews.map(view => [view.key, view])));
2940
+ }
2941
+
2942
+ const pagination = this.pagination; // Before the first answer there is no count to write. Writing one anyway
2943
+ // put «Всего: 0» under a heading that already said 450: the host's own
2944
+ // total in the markup was overwritten with this element's initial zero.
2945
+
2946
+ const counted = this._answered ? {
2947
+ total: this.total,
2948
+ countKnown: this.countKnown,
2949
+ totalExact: this.totalExact,
2950
+ hasNext: this.hasNext
2951
+ } : {}; // The page size in force is one of the sizes offered: 25 by default
2952
+ // against the pager's 10/20/30/40 showed a size the select could not
2953
+ // name (docs trial 4). An author's own page-sizes is kept, the size added.
2954
+
2955
+ const sizes = String(pagination?.getAttribute?.("page-sizes") ?? "10,20,30,40").split(/[\s,]+/u).map(Number).filter(size => Number.isInteger(size) && size > 0);
2956
+ const perPage = Number(this.query.per_page);
2957
+
2958
+ if (pagination && Number.isInteger(perPage) && perPage > 0 && !sizes.includes(perPage)) {
2959
+ pagination.setAttribute("page-sizes", [...sizes, perPage].sort((a, b) => a - b).join(","));
2960
+ }
2961
+
2962
+ pagination?.setState?.({
2963
+ current: this.query.page,
2964
+ pageSize: this.query.per_page,
2965
+ ...counted,
2966
+ canCount: this.knownCapability("count-on-request"),
2967
+ counting: false
2968
+ }); // The parts a consumer left out. Hidden rather than removed, so turning one
2969
+ // back on does not rebuild the shadow tree and lose what a host slotted
2970
+ // into it.
2971
+ // The shadow template is written once and cannot carry a language: it
2972
+ // is built before the element is anywhere. These two are named here,
2973
+ // where the page is readable.
2974
+
2975
+ const layout = this.shadowRoot.querySelector('[part="layout"]'); // The author's name for the view, when it has one: the generic «Представление
2976
+ // данных» replaced it (docs trial 4).
2977
+
2978
+ if (layout) layout.setAttribute("aria-label", this.getAttribute("aria-label") || (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "layoutLabel", this));
2979
+ const fallbackTable = this.shadowRoot.querySelector(`slot[name="content"] > ${(0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view-table")}`);
2980
+
2981
+ if (fallbackTable && !fallbackTable.getAttribute("aria-label")) {
2982
+ fallbackTable.setAttribute("aria-label", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "recordsLabel", this));
2983
+ }
2984
+
2985
+ const toolbar = this.shadowRoot.querySelector('[part="toolbar"]'); // A slotted table that draws its own toolbar (search, filter) needs no
2986
+ // second one: the view's fallback search stood beside the table's, two
2987
+ // searches in the documented minimal markup (docs trial 4).
2988
+
2989
+ const slottedTable = this.querySelector(`:scope > ${(0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view-table")}[slot="content"]`);
2990
+ const authorToolbar = slottedTable?.getAttribute("data-sf-host-toolbar") ?? slottedTable?.getAttribute("toolbar");
2991
+ const tableHasToolbar = Boolean(slottedTable) && authorToolbar !== "false";
2992
+ const ownToolbar = this.querySelector(':scope > [slot="toolbar"]');
2993
+ if (toolbar) toolbar.hidden = !this.renders("toolbar") || tableHasToolbar && !ownToolbar || this._queryRefused === true;
2994
+ const pageRow = this.shadowRoot.querySelector('slot[name="pagination"]'); // A tree has no pages: a level that is long ends in Show more, and the
2995
+ // host gives no total for the page row to count. Nor is there a page
2996
+ // row before the first answer or when there is no list to page: it
2997
+ // said «Всего: 10» over a loading, failed or refused list (docs trial 4).
2998
+
2999
+ const nothingToCount = !this._answered || this._queryRefused || ["error", "no-access"].includes(this.getAttribute("data-state"));
3000
+ if (pageRow) pageRow.hidden = !this.renders("pagination") || this.treeMode === "tree" || nothingToCount;
3001
+
3002
+ if (!this.capHidesRows) {
3003
+ if (this._showAll?.isConnected) this._showAll.remove();
3004
+ this._showAll = null;
3005
+ } else {
3006
+ const band = this.showAllBand;
3007
+ let button = band.querySelector("[data-show-all]");
3008
+
3009
+ if (!button) {
3010
+ button = document.createElement("sf-button");
3011
+ button.setAttribute("data-show-all", "");
3012
+ button.setAttribute("appearance", "link");
3013
+ button.setAttribute("size", "1");
3014
+ band.append(button);
3015
+ }
3016
+
3017
+ button.setAttribute("text", (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "showAll", this));
3018
+ let count = band.querySelector("[data-show-count]");
3019
+
3020
+ if (!count) {
3021
+ count = document.createElement("span");
3022
+ count.setAttribute("data-show-count", "");
3023
+ band.append(count);
3024
+ } // «N of M» only when the host has already said M. A count fetched to
3025
+ // decide what a link says would spend the host's query on this
3026
+ // element's own layout.
3027
+
3028
+
3029
+ count.textContent = this.countKnown ? (0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "ofTotal", this).replace("{shown}", String(this.cappedRecords.length)).replace("{total}", this._shownTotal(this.total)) : "";
3030
+ }
3031
+
3032
+ const search = this.shadowRoot.querySelector("[data-search]");
3033
+ if (search && search.value !== this.query.search) search.value = this.query.search;
3034
+ const select = this.shadowRoot.querySelector("[data-view]");
3035
+
3036
+ if (select) {
3037
+ const previous = select.value;
3038
+ select.replaceChildren(new Option((0,_core_js_page_language__WEBPACK_IMPORTED_MODULE_1__.pageText)(DATA_VIEW_TEXT, "currentViewText", this), ""), ...this.savedViews.map(view => new Option(view.label, view.key)));
3039
+ select.value = this.savedViews.some(view => view.key === previous) ? previous : "";
3040
+ }
3041
+ }
3042
+
3043
+ saveView({
3044
+ key,
3045
+ label,
3046
+ level = "personal",
3047
+ revision,
3048
+ roles,
3049
+ layout,
3050
+ settings
3051
+ } = {}) {
3052
+ if (!key || !label) throw new TypeError("Saved view needs key and label");
3053
+ if (!["personal", "shared"].includes(level)) throw new TypeError("Only personal and shared views can be written"); // Shared views came with port 1.1.0 and stay in every later edition: the
3054
+ // test was for 1.1.0 exactly, so 1.2.0 onwards refused them.
3055
+
3056
+ const [major, minor] = String(this.hostPort?.version ?? "0.0.0").split(".").map(Number);
3057
+
3058
+ if (level === "shared" && !(major > 1 || major === 1 && minor >= 1)) {
3059
+ return Promise.resolve({
3060
+ answer: "unavailable",
3061
+ reason: "saved-view-levels-require-port-1.1.0"
3062
+ });
3063
+ }
3064
+
3065
+ return this._raise(level === "shared" ? "view.save_shared" : "view.save", {
3066
+ key,
3067
+ label,
3068
+ revision,
3069
+ query: copy(this.query),
3070
+ roles: copy(roles ?? {}),
3071
+ layout: copy(layout ?? {
3072
+ columns: this.columns
3073
+ }),
3074
+ settings: copy(settings ?? this.columnSettings ?? {}),
3075
+ ...(level === "shared" ? {
3076
+ target: "shared"
3077
+ } : {})
3078
+ });
3079
+ }
3080
+
3081
+ deleteView(key) {
3082
+ return this._raise("view.delete", {
3083
+ key
3084
+ });
3085
+ } // Which view was opened, so its own settings can be put back when the
3086
+ // answer arrives: the fields the filter was showing are saved with it.
3087
+
3088
+
3089
+ openView(key) {
3090
+ this._openedView = key;
3091
+ return this._raise("view.open", {
3092
+ key
3093
+ });
3094
+ } // The part of a saved view that belongs to the table rather than to the
3095
+ // query: which filter fields it was showing.
3096
+
3097
+
3098
+ _restoreOpenedView() {
3099
+ const key = this._openedView;
3100
+ if (!key) return this;
3101
+ const view = this.savedViews.find(candidate => candidate?.key === key);
3102
+ if (!view) return this;
3103
+ this._openedView = null;
3104
+ const fields = view.settings?.filter_fields;
3105
+
3106
+ if (Array.isArray(fields)) {
3107
+ this.table?.setActiveFilterFields?.(fields.filter(item => typeof item === "string"));
3108
+ }
3109
+
3110
+ return this;
3111
+ }
3112
+
3113
+ saveColumnSettings(columnSettings, {
3114
+ shared = false,
3115
+ base_revision
3116
+ } = {}) {
3117
+ const layer = shared ? "shared" : "personal"; // The port's target.revision is the base when the host sent no layer of
3118
+ // that kind yet: the first personal save had none, and a column drag
3119
+ // threw an uncaught TypeError that never reached the host (docs trial 4).
3120
+
3121
+ const target = this.columnSettings?.target;
3122
+ const targetRevision = target && (target.kind ?? target.target ?? target) === layer ? target.revision : target?.revision ?? null;
3123
+ const revision = base_revision ?? this.columnSettings?.layers?.[layer]?.revision ?? targetRevision;
3124
+
3125
+ if (revision == null) {
3126
+ this._showAnswer(shared ? "settings.save_shared" : "settings.save_personal", {
3127
+ answer: "unavailable",
3128
+ reason: "no-base-revision"
3129
+ });
3130
+
3131
+ return Promise.resolve({
3132
+ answer: "unavailable",
3133
+ reason: "no-base-revision"
3134
+ });
3135
+ }
3136
+
3137
+ return this._raise(shared ? "settings.save_shared" : "settings.save_personal", {
3138
+ columnSettings: copy(columnSettings),
3139
+ base_revision: revision,
3140
+ ...(shared ? {
3141
+ target: "shared"
3142
+ } : {})
3143
+ }).then(answer => {
3144
+ // A conflict means the layer moved on: the revision this view holds
3145
+ // is stale, and every later change conflicted with it until a reload
3146
+ // (docs trial 5). The view takes the layer's current revision when
3147
+ // the host names it, and otherwise reads the list again, whose
3148
+ // answer carries the column settings as they are now.
3149
+ // The conflict carries current_revision (and the layer's columns)
3150
+ // beside answer, as port 1.12.0 states and every host gives it;
3151
+ // data.* is read too, for a host that followed 1.10.0's words.
3152
+ if (answer?.answer === "conflict") {
3153
+ const current = answer.current_revision ?? answer.data?.current_revision;
3154
+ const columns = answer.columns ?? answer.data?.columns;
3155
+ if (answer.data?.column_settings) this.columnSettings = copy(answer.data.column_settings);else if (current != null) {
3156
+ const layers = { ...(this.columnSettings?.layers ?? {})
3157
+ };
3158
+ layers[layer] = { ...(layers[layer] ?? {}),
3159
+ revision: current,
3160
+ ...(columns ? {
3161
+ columns: copy(columns)
3162
+ } : {})
3163
+ };
3164
+ this.columnSettings = { ...(this.columnSettings ?? {}),
3165
+ layers
3166
+ };
3167
+ } else this.refresh("settings-conflict");
3168
+ }
3169
+
3170
+ return answer;
3171
+ });
3172
+ } // Resetting the person's layer is a write of nothing against its revision,
3173
+ // which is exactly what the port calls a reset: "a write against its
3174
+ // revision, never a deletion". The empty object matters — the host owns the
3175
+ // defaults, and writing this component's idea of them (everything visible,
3176
+ // no widths, nothing pinned) would overwrite them.
3177
+
3178
+
3179
+ resetColumnSettings() {
3180
+ return this.saveColumnSettings({}, {});
3181
+ } // An estimate reads «~N» wherever this element writes the number itself.
3182
+
3183
+
3184
+ _shownTotal(total) {
3185
+ return this.totalExact ? String(total) : `~${total}`;
3186
+ }
3187
+
3188
+ applyUnderFilter(actionId, confirmedTotal) {
3189
+ // An estimate is not a number anyone can confirm: count first, show the
3190
+ // exact number and stop. The person confirmed «~N», not this number,
3191
+ // so nothing is written until they confirm it (standard 1.3.8 §19.4;
3192
+ // stand 24 S15: the counted number went to the host at once, more
3193
+ // records than the person agreed to when the estimate was low). The
3194
+ // answer is the conflict a moved count gives: current_total is the
3195
+ // number to confirm.
3196
+ if (!this.totalExact) {
3197
+ return this.count("action-for-all").then(answer => {
3198
+ if (!(this.totalExact && answer?.answer === "applied")) return answer;
3199
+
3200
+ this._renderBulkBand();
3201
+
3202
+ return {
3203
+ answer: "conflict",
3204
+ reason: "total-counted",
3205
+ current_total: this.total
3206
+ };
3207
+ });
3208
+ }
3209
+
3210
+ if (!Number.isSafeInteger(confirmedTotal) || confirmedTotal !== this.total) {
3211
+ throw new TypeError("Confirm the current total before a filter-wide action");
3212
+ } // A count that moved meanwhile is a conflict carrying the current total
3213
+ // (vocabulary §3.1): the page row shows that number at once, so the
3214
+ // person confirms what is there now, not what was.
3215
+
3216
+
3217
+ return this._raise("bulk.apply_under_filter", {
3218
+ action_id: actionId,
3219
+ filters: copy(this.query.filters),
3220
+ confirmed_total: confirmedTotal
3221
+ }).then(answer => {
3222
+ const current = answer?.current_total ?? answer?.data?.total;
3223
+
3224
+ if (answer?.answer === "conflict" && Number.isSafeInteger(current) && current >= 0) {
3225
+ this.total = current;
3226
+ this.totalExact = true;
3227
+ this.pagination?.setState?.({
3228
+ total: current,
3229
+ countKnown: true,
3230
+ totalExact: true
3231
+ });
3232
+
3233
+ this._renderBulkBand();
3234
+ }
3235
+
3236
+ return answer;
3237
+ });
3238
+ } // The number, asked for on its own. The view resumes whatever wanted it:
3239
+ // showing it, jumping to the last page, or an action under the filter that
3240
+ // has to name a confirmed total. A refusal puts the view back where it was
3241
+ // and says so — a for-all box left ticked over a number nobody has would be
3242
+ // a promise the view cannot keep.
3243
+
3244
+
3245
+ async count(reason = "total") {
3246
+ const requested = this._countKey();
3247
+
3248
+ const answer = await this._raise("query.count", {
3249
+ query: {
3250
+ search: this.query.search ?? "",
3251
+ filters: copy(this.query.filters ?? {})
3252
+ },
3253
+ reason
3254
+ });
3255
+ const total = answer?.data?.total;
3256
+
3257
+ if (answer?.answer !== "applied" || !Number.isSafeInteger(total) || total < 0) {
3258
+ this.pagination?.setState?.({
3259
+ counting: false
3260
+ });
3261
+ if (reason === "action-for-all") this.pagination?.setState?.({
3262
+ actionForAll: false
3263
+ });
3264
+ return answer;
3265
+ } // The query may have moved on while the host was counting; a number
3266
+ // belonging to a query nobody is looking at any more is not shown.
3267
+
3268
+
3269
+ if (requested !== this._countKey()) {
3270
+ this.pagination?.setState?.({
3271
+ counting: false
3272
+ });
3273
+ return answer;
3274
+ }
3275
+
3276
+ this.total = total;
3277
+ this.countKnown = true;
3278
+ this.totalExact = answer.data.total_exact !== false;
3279
+ this.countedQuery = requested;
3280
+ this.pagination?.setState?.({
3281
+ counting: false,
3282
+ countKnown: true,
3283
+ totalExact: this.totalExact,
3284
+ total
3285
+ });
3286
+ if (reason === "last-page") this.pagination?.lastPage?.();
3287
+ return answer;
3288
+ }
3289
+
3290
+ _action(detail, {
3291
+ selectedBulk = false
3292
+ } = {}) {
3293
+ const ids = detail?.record_ids;
3294
+ if (!Array.isArray(ids) || !ids.length || typeof detail.action_id !== "string") return;
3295
+ const chosen = ids.map(id => this.records.find(record => record.id === id)); // In a tree two of a record's actions are the view's own: move opens
3296
+ // the choice of the places the host offers, create makes a node under
3297
+ // it. Each is offered only when the record lists it.
3298
+
3299
+ if (this.treeMode === "tree" && !selectedBulk && ids.length === 1 && chosen[0]?.actions?.includes(detail.action_id)) {
3300
+ if (detail.action_id === "move") return this.openMoveDialog(chosen[0]);
3301
+ if (detail.action_id === "create") return this._raise("view.create_record", {
3302
+ item: null,
3303
+ parent: chosen[0].id
3304
+ });
3305
+ } // A selected-bulk action has its own ID and permission in the host, even
3306
+ // when just one row is selected. The host checks the whole batch.
3307
+
3308
+
3309
+ if (chosen.some(record => !record) || !selectedBulk && ids.length === 1 && !chosen[0].actions?.includes(detail.action_id)) {
3310
+ this._showAnswer("record.mutate", {
3311
+ answer: "refused",
3312
+ reason: "action-not-allowed"
3313
+ });
3314
+
3315
+ return;
3316
+ } // Opening a record is a request a page or an assembly takes
3317
+ // (vocabulary §5 open {id}): sf-data-view-open, then the host's
3318
+ // record.open unless a listener refused it (preventDefault).
3319
+
3320
+
3321
+ if (!selectedBulk && ids.length === 1 && detail.action_id === "view") {
3322
+ const allowed = this.sfEmit("open", {
3323
+ id: ids[0]
3324
+ }, {
3325
+ cancelable: true
3326
+ });
3327
+ return allowed ? this._raise("record.open", detail) : null;
3328
+ }
3329
+
3330
+ const revisions = Object.fromEntries(chosen.map(record => [String(record.id), record.revision]));
3331
+
3332
+ const written = this._raise(selectedBulk || ids.length > 1 ? "bulk.apply_selected" : "record.mutate", { ...detail,
3333
+ revisions
3334
+ }); // The fact a screen closes a deleted record on (vocabulary §4: a
3335
+ // fact after applied): sf-data-view-deleted {ids}.
3336
+
3337
+
3338
+ if (detail.action_id === "delete") {
3339
+ return Promise.resolve(written).then(answer => {
3340
+ if (answer?.answer === "applied") this.sfEmit("deleted", {
3341
+ ids: [...ids]
3342
+ });
3343
+ return answer;
3344
+ });
3345
+ }
3346
+
3347
+ return written;
3348
+ } // A value pointing at another record opens it in the host's panel without
3349
+ // loading a page (owner, 2026-10-14). Taken only when the host opens
3350
+ // records; when its answer is not an opening, the address is followed, so
3351
+ // the click still goes somewhere.
3352
+
3353
+
3354
+ _openReference(event) {
3355
+ if (!this.hostPort || !this.hostOffers("row-actions")) return;
3356
+ event.preventDefault(); // The table's detail is camelCase on the element (standard 1.3.8 §3.2);
3357
+ // the port's payload is snake_case.
3358
+
3359
+ const {
3360
+ href,
3361
+ recordId,
3362
+ record_id: recordIdBefore,
3363
+ ...detail
3364
+ } = event.detail ?? {};
3365
+ return this._raise("record.open", { ...detail,
3366
+ record_id: recordId ?? recordIdBefore ?? null,
3367
+ href
3368
+ }).then(answer => {
3369
+ if (!["applied", "accepted", "opened"].includes(answer?.answer) && href) globalThis.location?.assign?.(href);
3370
+ });
3371
+ }
3372
+
3373
+ _onEvent(event) {
3374
+ if (this._seenEvents.has(event)) return;
3375
+
3376
+ this._seenEvents.add(event); // The port's event for a shared saved view (view.save_shared): a page
3377
+ // raises it on the view itself, so it is taken before the rule that
3378
+ // ignores the view's own events. It was declared and never listened to.
3379
+
3380
+
3381
+ if (event.type === "sf-view-saved-view-intent") {
3382
+ if (event.currentTarget !== this) return;
3383
+ const view = event.detail ?? {};
3384
+ return this.saveView({
3385
+ key: view.key,
3386
+ label: view.label ?? view.key,
3387
+ revision: view.revision,
3388
+ level: "shared",
3389
+ roles: view.roles
3390
+ });
3391
+ }
3392
+
3393
+ if (event.target === this || event.target?.closest?.((0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.tagSelector)("sf-view")) !== this && !this.shadowRoot.contains(event.target)) return; // An event crossing the shadow boundary is observed once, at the shadow root.
3394
+
3395
+ if (event.currentTarget === this && this.shadowRoot.contains(event.target)) return;
3396
+ if (event.type === "change" && event.target.matches?.("[data-search]")) return this.changeQuery({
3397
+ search: event.target.value
3398
+ }, "search");
3399
+ if (event.type === "change" && event.target.matches?.("[data-view]")) return event.target.value && this.openView(event.target.value);
3400
+ if (event.type === "click" && event.target.matches?.("[data-show-all]")) return this.openList("row-cap");
3401
+
3402
+ if (event.type === "click" && event.target.matches?.("[data-save]")) {
3403
+ const label = this.shadowRoot.querySelector("[data-view-name]")?.value?.trim();
3404
+ if (label) return this.saveView({
3405
+ key: label,
3406
+ label
3407
+ });
3408
+ }
3409
+
3410
+ if (event.type === "click" && event.target.matches?.("[data-delete]")) {
3411
+ const key = this.shadowRoot.querySelector("[data-view]").value;
3412
+ if (key) return this.deleteView(key);
3413
+ }
3414
+
3415
+ if (event.type === "sf-view-table-cell-set") return this.setCell(event.detail ?? {});
3416
+ if (event.type === "sf-view-table-move") return this.reorderRows(event.detail ?? {});
3417
+
3418
+ if (event.type === "sf-view-table-search-change") {
3419
+ const search = typeof event.detail === "object" && event.detail !== null ? event.detail.search : event.detail;
3420
+ return this.changeQuery({
3421
+ search: String(search ?? "")
3422
+ }, "search");
3423
+ }
3424
+
3425
+ if (event.type === "sf-view-table-filter-change") return this.changeQuery({
3426
+ filters: copy(event.detail?.filters ?? event.detail?.values ?? event.detail ?? {})
3427
+ }, "filter");
3428
+
3429
+ if (event.type === "sf-view-table-sort-change") {
3430
+ const sort = event.detail?.direction ? [copy(event.detail)] : [];
3431
+
3432
+ this._keepPreference({
3433
+ sort
3434
+ });
3435
+
3436
+ return this.changeQuery({
3437
+ sort
3438
+ }, "sort");
3439
+ }
3440
+
3441
+ if (event.type === "sf-pagination-change") return this.changeQuery({
3442
+ page: event.detail?.current
3443
+ }, "page-change");
3444
+
3445
+ if (event.type === "sf-pagination-size-change") {
3446
+ const size = event.detail?.["page-size"];
3447
+ if (Number.isInteger(size)) this._keepPreference({
3448
+ pageSize: size
3449
+ }); // Another page size reads the list from its first page.
3450
+
3451
+ return this.changeQuery({
3452
+ per_page: size,
3453
+ page: 1
3454
+ }, "page-size-change");
3455
+ }
3456
+
3457
+ if (event.type === "sf-pagination-load-more") return this.changeQuery({
3458
+ page: this.query.page + 1
3459
+ }, "show-more");
3460
+ if (event.type === "sf-pagination-count") return this.count(event.detail?.reason ?? "total");
3461
+ if (event.type === "sf-view-table-column-settings-reset") return this.resetColumnSettings(); // The panel's cross says the selection is over. The rows hold it, so
3462
+ // the view passes the word on rather than tracking a copy of its own.
3463
+
3464
+ if (event.type === "sf-pagination-selection-clear") return this.clearSelection();
3465
+
3466
+ if (event.type === "sf-pagination-action-change" || event.type === "sf-pagination-action-for-all-change") {
3467
+ // The chooser and For-all live in the band now; the pagination still
3468
+ // holds their state, so the band is redrawn when it changes.
3469
+ this._renderBulkBand();
3470
+
3471
+ return undefined;
3472
+ }
3473
+
3474
+ if (event.type === "sf-view-table-refresh") return this.refresh("retry");
3475
+ if (event.type === "sf-view-table-reference-open") return this._openReference(event);
3476
+
3477
+ if (event.type === "sf-view-table-expand") {
3478
+ const id = event.detail?.id;
3479
+ return event.detail?.expanded ? this.collapseNode(id) : this.expandNode(id);
3480
+ }
3481
+
3482
+ if (event.type === "sf-view-table-load-more") return this.loadMoreNodes(event.detail?.parent, event.detail?.cursor);
3483
+ if (event.type === "sf-view-table-apply") return this._action({
3484
+ action_id: event.detail?.action,
3485
+ record_ids: event.detail?.ids
3486
+ }, {
3487
+ selectedBulk: event.detail?.scope === "selection"
3488
+ });
3489
+
3490
+ if (event.type === "sf-view-table-save") {
3491
+ return this.saveColumnSettings(event.detail?.columnSettings ?? event.detail, {
3492
+ shared: event.detail?.target === "shared",
3493
+ base_revision: event.detail?.base_revision
3494
+ });
3495
+ }
3496
+
3497
+ if (event.type === "sf-pagination-apply") {
3498
+ if (event.detail?.scope === "filter") return this.applyUnderFilter(event.detail.action, event.detail.total);
3499
+ return this._action({
3500
+ action_id: event.detail?.action,
3501
+ record_ids: this.table?.getSelectedRecordIds?.() ?? []
3502
+ }, {
3503
+ selectedBulk: true
3504
+ });
3505
+ }
3506
+
3507
+ if (event.type === "sf-view-table-select") {
3508
+ this.pagination?.setState?.({
3509
+ selectedCount: event.detail?.ids?.length ?? 0
3510
+ });
3511
+
3512
+ this._renderBulkBand();
3513
+
3514
+ return;
3515
+ }
3516
+
3517
+ const intent = EVENTS[event.type];
3518
+ if (intent === "query.change") return this.refresh("table-context");
3519
+
3520
+ if (intent === "view.save") {
3521
+ // Reordering or pinning on the templates tab sends every changed
3522
+ // template at once, as an array: each one is its own save (it threw
3523
+ // «Saved view needs key and label» on the array itself).
3524
+ const save = (view = {}) => this.saveView({
3525
+ key: view.key,
3526
+ label: view.label ?? view.name ?? view.key,
3527
+ revision: view.revision,
3528
+ level: view.level ?? "personal",
3529
+ roles: view.roles,
3530
+ layout: view.layout,
3531
+ settings: view.settings
3532
+ });
3533
+
3534
+ const detail = event.detail ?? {};
3535
+ return Array.isArray(detail) ? Promise.all(detail.filter(view => view?.key).map(save)) : save(detail);
3536
+ }
3537
+
3538
+ if (intent) return this._raise(intent, copy(event.detail ?? {}));
3539
+ }
3540
+
3541
+ } // Not a Smart base element, so it carries the same brand by hand: quiet when
3542
+ // the component arrives twice, loud when something else holds the tag.
3543
+
3544
+
3545
+ SfDataView.sfElementBrand = "simai.smart-base.v1";
3546
+ (0,_view_table_js_manifest_actions_js__WEBPACK_IMPORTED_MODULE_5__.answerActions)(SfDataView, _component_json__WEBPACK_IMPORTED_MODULE_6__);
3547
+ (0,_view_table_js_manifest_actions_js__WEBPACK_IMPORTED_MODULE_5__.settingSetters)(SfDataView, _component_json__WEBPACK_IMPORTED_MODULE_6__);
3548
+ const definedDataView = customElements.get("sf-view");
3549
+
3550
+ if (!definedDataView) {
3551
+ customElements.define("sf-view", SfDataView); // sf-data-view, the former tag, is a subclass defined beside it until
3552
+ // 2027-01-01 (aliases.json, ui-source change journal tag-sf-data-view-to-sf-view).
3553
+
3554
+ (0,_core_js_smart_base__WEBPACK_IMPORTED_MODULE_3__.defineTagAliases)(SfDataView, "sf-view");
3555
+ } else if (definedDataView.sfElementBrand !== SfDataView.sfElementBrand) {
3556
+ console.warn("SfDataView.define(): sf-view is already defined", definedDataView);
3557
+ }
3558
+
3559
+ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (SfDataView);
3560
+ __webpack_async_result__();
3561
+ } catch(e) { __webpack_async_result__(e); } });
3562
+
3563
+ /***/ },
3564
+
3565
+ /***/ "29f88c884ba1"
3566
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
3567
+
3568
+ __webpack_require__.r(__webpack_exports__);
3569
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
3570
+ /* harmony export */ ROOT: () => (/* binding */ ROOT),
3571
+ /* harmony export */ acceptLevel: () => (/* binding */ acceptLevel),
3572
+ /* harmony export */ collapse: () => (/* binding */ collapse),
3573
+ /* harmony export */ createTree: () => (/* binding */ createTree),
3574
+ /* harmony export */ dropLevel: () => (/* binding */ dropLevel),
3575
+ /* harmony export */ expandKnown: () => (/* binding */ expandKnown),
3576
+ /* harmony export */ hasLevel: () => (/* binding */ hasLevel),
3577
+ /* harmony export */ knownRecords: () => (/* binding */ knownRecords),
3578
+ /* harmony export */ markLoading: () => (/* binding */ markLoading),
3579
+ /* harmony export */ visibleRows: () => (/* binding */ visibleRows)
3580
+ /* harmony export */ });
3581
+ // The tree a data view shows when its host answers in levels (data view port
3582
+ // 1.8.0). The host answers one level at a time -- the roots, or one node's
3583
+ // children after tree.expand -- and the view keeps what it has been told and
3584
+ // which nodes are open. The rows the table draws are a walk of that, in order,
3585
+ // each row knowing its level and its place among its siblings; nothing here
3586
+ // builds a tree out of a flat dump or decides where a node may go.
3587
+ const ROOT = "\u0000root";
3588
+
3589
+ const keyOf = parent => parent === null || parent === undefined ? ROOT : String(parent);
3590
+
3591
+ function createTree() {
3592
+ return {
3593
+ nodes: new Map(),
3594
+ children: new Map(),
3595
+ expanded: new Set(),
3596
+ loading: new Set()
3597
+ };
3598
+ }
3599
+ /**
3600
+ * One level the host answered: the roots (parent null) or a node's children.
3601
+ * `append` keeps the children already known and adds the next page of them.
3602
+ */
3603
+
3604
+ function acceptLevel(tree, {
3605
+ parent = null,
3606
+ records = [],
3607
+ hasNext = false,
3608
+ cursor = null,
3609
+ append = false
3610
+ } = {}) {
3611
+ const key = keyOf(parent);
3612
+
3613
+ if (key === ROOT && !append) {
3614
+ tree.nodes.clear();
3615
+ tree.children.clear();
3616
+ tree.loading.clear(); // Open nodes stay open across a re-read: a person who opened a branch
3617
+ // and saved a row does not want it closed under them. They are dropped
3618
+ // when the node is gone, by the walk, which never reaches them.
3619
+ }
3620
+
3621
+ for (const record of records) tree.nodes.set(String(record.id), record);
3622
+
3623
+ const before = append ? tree.children.get(key)?.ids ?? [] : [];
3624
+ const seen = new Set(before);
3625
+ const ids = [...before, ...records.map(record => String(record.id)).filter(id => !seen.has(id))];
3626
+ tree.children.set(key, {
3627
+ ids,
3628
+ hasNext: Boolean(hasNext),
3629
+ cursor: cursor ?? null
3630
+ });
3631
+
3632
+ if (key !== ROOT) {
3633
+ tree.expanded.add(key);
3634
+ tree.loading.delete(key);
3635
+ }
3636
+
3637
+ return tree;
3638
+ }
3639
+ /** Whether a node's children are already known, so opening it needs no host. */
3640
+
3641
+ function hasLevel(tree, id) {
3642
+ return tree.children.has(keyOf(id));
3643
+ }
3644
+ function collapse(tree, id) {
3645
+ tree.expanded.delete(String(id));
3646
+ return tree;
3647
+ }
3648
+ function expandKnown(tree, id) {
3649
+ tree.expanded.add(String(id));
3650
+ return tree;
3651
+ }
3652
+ function markLoading(tree, id) {
3653
+ tree.loading.add(String(id));
3654
+ return tree;
3655
+ }
3656
+ /** Forget a node's children, so the next opening asks the host again. */
3657
+
3658
+ function dropLevel(tree, id) {
3659
+ tree.children.delete(keyOf(id));
3660
+ tree.expanded.delete(String(id));
3661
+ return tree;
3662
+ }
3663
+ /**
3664
+ * The rows to draw: the known tree walked from the roots through every open
3665
+ * node. A row is the record with `__tree` beside it -- the level from 0, whether
3666
+ * it has children and is open, and its place among its siblings for
3667
+ * aria-setsize and aria-posinset. A level with more to come ends in a row of
3668
+ * its own, `__tree.more`, which asks for the next page.
3669
+ */
3670
+
3671
+ function visibleRows(tree) {
3672
+ const rows = [];
3673
+
3674
+ const walk = (key, level) => {
3675
+ const entry = tree.children.get(key);
3676
+ if (!entry) return;
3677
+ entry.ids.forEach((id, index) => {
3678
+ const record = tree.nodes.get(id);
3679
+ if (!record) return;
3680
+ const expanded = tree.expanded.has(id) && tree.children.has(id);
3681
+ rows.push({ ...record,
3682
+ __tree: {
3683
+ level: Number.isInteger(record.level) ? record.level : level,
3684
+ hasChildren: record.has_children === true,
3685
+ expanded,
3686
+ loading: tree.loading.has(id),
3687
+ setsize: entry.ids.length,
3688
+ posinset: index + 1
3689
+ }
3690
+ });
3691
+ if (expanded) walk(id, level + 1);
3692
+ });
3693
+
3694
+ if (entry.hasNext) {
3695
+ rows.push({
3696
+ id: `__more:${key === ROOT ? "" : key}`,
3697
+ __tree: {
3698
+ level,
3699
+ more: true,
3700
+ parent: key === ROOT ? null : key,
3701
+ cursor: entry.cursor
3702
+ }
3703
+ });
3704
+ }
3705
+ };
3706
+
3707
+ walk(ROOT, 0);
3708
+ return rows;
3709
+ }
3710
+ /** Every record the tree knows, open or not: what an action is checked against. */
3711
+
3712
+ function knownRecords(tree) {
3713
+ return [...tree.nodes.values()];
3714
+ }
3715
+
3716
+ /***/ },
3717
+
3718
+ /***/ "4d9330f13af8"
3719
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
3720
+
3721
+ __webpack_require__.r(__webpack_exports__);
3722
+ // extracted by mini-css-extract-plugin
3723
+
3724
+
3725
+ /***/ },
3726
+
3727
+ /***/ "892a0da25239"
3728
+ (module) {
3729
+
3730
+ module.exports = (globalThis.SF && globalThis.SF.runtimeModule ? globalThis.SF.runtimeModule("aliases", 1) : new Promise((resolve, reject) => globalThis.addEventListener("sf-runtime-ready", () => globalThis.SF.runtimeModule("aliases", 1).then(resolve, reject), { once: true })));
3731
+
3732
+ /***/ },
3733
+
3734
+ /***/ "5b7d7695b41b"
3735
+ (module) {
3736
+
3737
+ module.exports = (globalThis.SF && globalThis.SF.runtimeModule ? globalThis.SF.runtimeModule("element-manifest", 1) : new Promise((resolve, reject) => globalThis.addEventListener("sf-runtime-ready", () => globalThis.SF.runtimeModule("element-manifest", 1).then(resolve, reject), { once: true })));
3738
+
3739
+ /***/ },
3740
+
3741
+ /***/ "43090d1c8307"
3742
+ (module) {
3743
+
3744
+ module.exports = (globalThis.SF && globalThis.SF.runtimeModule ? globalThis.SF.runtimeModule("page-language", 1) : new Promise((resolve, reject) => globalThis.addEventListener("sf-runtime-ready", () => globalThis.SF.runtimeModule("page-language", 1).then(resolve, reject), { once: true })));
3745
+
3746
+ /***/ },
3747
+
3748
+ /***/ "deb1a4d61565"
3749
+ (module) {
3750
+
3751
+ module.exports = (globalThis.SF && globalThis.SF.runtimeModule ? globalThis.SF.runtimeModule("smart-base", 1) : new Promise((resolve, reject) => globalThis.addEventListener("sf-runtime-ready", () => globalThis.SF.runtimeModule("smart-base", 1).then(resolve, reject), { once: true })));
3752
+
3753
+ /***/ },
3754
+
3755
+ /***/ "80ad38cb6908"
3756
+ (module) {
3757
+
3758
+ module.exports = /*#__PURE__*/JSON.parse('{"$comment":"Dated aliases of sf-view for the shim (core/js/aliases.js, standard §16 item 20).","component":"sf-view","since":"2026-10-27","remove_after":"2026-12-01","aliases":[{"from":"aria-label","to":null,"kind":"attribute","component":"sf-view","since":"2026-10-27","remove_after":"2026-12-01","mode":"host-attribute","section":"§3.1, §4.8","note":"stays a host attribute, read for the accessible name; no shim"},{"from":"sf-table-tree-intent","to":"sf-data-view-tree","kind":"event","component":"sf-view","since":"2026-10-27","remove_after":"2026-12-01","mode":"same-detail","cancelable":false,"section":"§8, vocabulary §9","note":"the view\'s announcement of the tree\'s port intents carried a table name; the detail {intent, payload} is unchanged"},{"from":"sf-data-view","to":"sf-view","kind":"tag","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","section":"rename plan R2 (ui-control RENAME-PLAN-2026-11-03), naming decision 2026-11-02","note":"the element is sf-view; sf-data-view is a subclass defined beside it until the date"},{"from":"sf-data-view-open","to":"sf-view-open","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-deleted","to":"sf-view-deleted","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-answer","to":"sf-view-answer","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-state","to":"sf-view-state","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-tree","to":"sf-view-tree","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-files-drop","to":"sf-view-files-drop","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-saved-view-intent","to":"sf-view-saved-view-intent","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"},{"from":"sf-data-view-list-open","to":"sf-view-list-open","kind":"event","component":"sf-view","since":"2026-11-03","remove_after":"2027-01-01","mode":"same-detail","section":"§8","note":"the event name carries the tag"}]}');
3759
+
3760
+ /***/ },
3761
+
3762
+ /***/ "7086146c00cc"
3763
+ (module) {
3764
+
3765
+ module.exports = /*#__PURE__*/JSON.parse('{"id":"smart.view","tag":"sf-view","version":"1.8.1","kind":"leaf","settings":{"chrome":{"type":"string","default":"toolbar pagination select settings"},"row-cap":{"type":"number","default":0,"integer":true,"min":0},"expected-columns":{"type":"number","default":4,"integer":true,"min":1},"port-owner":{"type":"string","default":""},"port-record":{"type":"string","default":""},"port-group":{"type":"string","default":""},"size":{"type":"enum","values":["1/2","1"],"default":"1"},"column-search-placeholder":{"type":"string"},"column-search-empty-text":{"type":"string"},"column-move-label":{"type":"string"},"filter-apply-text":{"type":"string"},"show-more-text":{"type":"string"},"selected-label":{"type":"string"},"total-label":{"type":"string"},"pages-label":{"type":"string"},"first-label":{"type":"string"},"previous-label":{"type":"string"},"page-label":{"type":"string"},"next-label":{"type":"string"},"last-label":{"type":"string"},"page-field-label":{"type":"string"},"action-choose-label":{"type":"string"},"actions-label":{"type":"string"},"actions-region-label":{"type":"string"},"clear-selection-label":{"type":"string"},"page-size-label":{"type":"string"},"pagination-aria-label":{"type":"string"},"preferences-key":{"type":"string","default":""},"accept-files":{"type":"boolean","default":false},"collection":{"type":"string","default":""},"current":{"type":"string","default":""},"bulk-actions":{"type":"list","of":{"id":"string","label":"string"}},"first-answer":{"type":"object","optional":true}},"values":{"state":{"type":"state"},"total":{"type":"number","nullable":true},"selected":{"type":"id","optional":true},"selection":{"type":"list"}},"events":{"open":{"carries":{"id":"id"},"cancelable":true},"deleted":{"carries":{"ids":"list"}},"answer":{"carries":{"intent":{"type":"string"},"answer":{"type":"enum","values":["applied","conflict","refused","unavailable"],"schema":{"type":"string"}},"reason":{"type":"string","optional":true},"message":{"type":"string","optional":true},"data":{"type":"object","optional":true},"sequence":{"type":"number","integer":true,"optional":true},"effects":{"type":"list","optional":true},"instance_id":{"type":"string"}},"own-verb":true},"state":{"carries":{"state":{"type":"enum","values":["loading","populated","empty","error","no-access"],"schema":{"type":"string"}},"message":{"type":"string"},"instance_id":{"type":"string"}},"own-verb":true},"tree":{"carries":{"intent":"string","payload":{"type":"object","optional":true}},"own-verb":true},"files-drop":{"carries":{"files":{"type":"list","schema":{"items":{"type":"object"},"minItems":1}}},"own-verb":true},"pagination-count":{"shared":"count-on-request@1","carries":{"component":{"type":"object"},"tagName":{"type":"string"},"template":{"type":"string"},"reason":{"type":"enum","values":["total","last-page","action-for-all"],"schema":{"type":"string"}},"current":{"type":"number","integer":true},"pageSize":{"type":"number","integer":true}}},"saved-view-intent":{"carries":{"key":{"type":"string"},"label":{"type":"string"},"revision":{"type":"string","schema":{"type":["string","integer"]},"optional":true},"roles":{"type":"object","optional":true}}},"table-action-intent":{"shared":"table-intents@1","carries":{"action_id":{"type":"string","schema":{"minLength":1}},"record_ids":{"type":"list","schema":{"items":{"type":["string","integer"]},"minItems":1}}}},"table-column-settings-change":{"shared":"table-intents@1","carries":{"reason":{"type":"string"},"columnSettings":{"type":"object"},"tableSettingsKey":{"type":"string"},"target":{"type":"enum","values":["personal","shared"],"schema":{"type":"string"},"optional":true},"base_revision":{"type":"string","schema":{"type":["string","number"]},"optional":true}}},"table-column-settings-reset":{"shared":"table-intents@1","carries":{"target":{"type":"enum","values":["personal"],"schema":{"type":"string"}},"tableSettingsKey":{"type":"string","schema":{"type":["string","null"]}}}},"table-create-intent":{"shared":"table-intents@1","carries":{"item":{"type":"string","schema":{"type":["string","null"]}}}},"table-query-intent":{"shared":"table-intents@1","carries":{"reason":{"type":"enum","values":["context"]},"context":{"type":"object"},"sequence":{"type":"number","integer":true,"schema":{"minimum":1}}}},"table-template-delete":{"shared":"table-intents@1","carries":{"key":{"type":"string","schema":{"minLength":1}}}},"table-template-save":{"shared":"table-intents@1","carries":{},"x-comment":"The detail is one template object or an array of them: a carries map cannot say that, so the payload schema is x-registry/event-fields until 1.4 has a home for a detail that is not a flat object."},"table-template-select":{"shared":"table-intents@1","carries":{"key":{"type":"string","schema":{"minLength":1}}}},"host-port-request":{"shared":"host-port@1","carries":{"component":{"type":"object","optional":true},"context":{"type":"object","optional":true}}},"list-open":{"carries":{"reason":{"type":"id","schema":{"enum":["row-cap"]}},"query":{"type":"object"}}},"table-cell-set":{"shared":"table-intents@1","carries":{"id":{"type":"string","schema":{"type":["string","integer"]}},"key":{"type":"string"},"value":{"type":"id"},"revision":{"type":"string","schema":{"type":["string","integer","null"]}}}},"table-rows-reorder":{"shared":"table-intents@1","carries":{"ids":{"type":"list","schema":{"items":{"type":"string"},"minItems":1}},"before":{"type":"string","schema":{"type":["string","null"]}},"order":{"type":"list","schema":{"items":{"type":"string"}}}}}},"actions":{"refresh":{"with":{"reason":{"type":"string","optional":true}}},"change-query":{"with":{"patch":"object","reason":{"type":"string","optional":true}},"concurrency":"latest"},"open-view":{"with":{"key":"string"}},"save-view":{"with":{"view":{"type":"object"}},"concurrency":"drop"},"delete-view":{"with":{"key":"string"},"concurrency":"drop"},"apply-under-filter":{"with":{"action-id":"string","confirmed-total":"number"},"concurrency":"drop"},"reset-column-settings":{},"clear-selection":{},"notify":{"with":{"text":"string","options":{"type":"object","optional":true}}}},"port":{"id":"simai.dataview-port","version":"1.13.0","contract":"src/core/contracts/ports/simai.dataview-port/1.13.0/port.json"},"presentation":{"skeleton":{"late":[]}},"css-variables":{"--sf-data-view-gap":{"type":"length","state":"default"},"--sf-data-view-drop-border-color":{"type":"color","state":"dragged-over"},"--sf-data-view-drop-background-color":{"type":"color","state":"dragged-over"},"--sf-data-view-drop-color":{"type":"color","state":"dragged-over"},"--sf-data-view-toolbar-gap":{"type":"length","state":"default"}},"host-states":{"data-state":"string","aria-busy":"string"}}');
3766
+
3767
+ /***/ },
3768
+
3769
+ /***/ "ce0305e57fa5"
3770
+ (module) {
3771
+
3772
+ module.exports = /*#__PURE__*/JSON.parse('{"title":"Data view","events":{"answer":{"label":"The host answered"},"state":{"label":"List state"},"tree":{"label":"Tree request"},"files-drop":{"label":"Files dropped on the list"}},"runtime":{"cancel":"Cancel","close-notice-label":"Close the message","confirm-apply":"Confirm","confirm-title":"Confirm the change","create-child-action":"Add a nested one","current-view-text":"Current","drop-files":"Drop the files here to upload them","empty-text":"No data","layout-label":"Data view","move-action":"Move…","move-apply":"Move","move-first":"First","move-last":"Last","move-no-targets":"There is nowhere this record can be moved.","move-place":"Place","move-root":"To the top level","move-title":"Move «{label}»","notice-action-not-allowed":"That action is not available for the chosen record.","notice-capability-not-provided":"The host does not support that action.","notice-confirm-required":"The action has to be confirmed first.","notice-conflict":"The data changed. Read the view again and repeat the action.","notice-failed":"The action was not carried out.","notice-fetch-failed":"The data could not be fetched. Try again.","notice-forbidden":"You may not do this.","notice-load-failed":"The data could not be loaded.","notice-no-base-revision":"The settings cannot be saved yet: the view has no current version of them. Read the view again.","notice-not-confirmed":"The action was cancelled.","notice-port-not-connected":"The data source is not connected.","notice-record-invalid":"The record was not found.","notice-refused":"The action was refused.","notice-saved-view-levels":"Shared views need a newer port.","notice-stale-revision":"Someone changed this in the meantime. Read the view again and repeat the action.","notice-unavailable":"The action is unavailable.","of-total":"{shown} of {total}","records-label":"Records","show-all":"Show all","state-load-failed":"The data could not be loaded","state-loading":"Loading","state-port-not-connected":"The data port is not connected","state-query-failed":"The request could not be completed","state-unavailable":"The data is temporarily unavailable","unnamed-action":"Action"}}');
3773
+
3774
+ /***/ },
3775
+
3776
+ /***/ "3e9dffe84919"
3777
+ (module) {
3778
+
3779
+ module.exports = /*#__PURE__*/JSON.parse('{"title":"Представление данных","events":{"answer":{"label":"Ответ хоста"},"state":{"label":"Состояние списка"},"tree":{"label":"Запрос ветви дерева"},"files-drop":{"label":"Файлы брошены на список"}},"runtime":{"cancel":"Отмена","close-notice-label":"Закрыть сообщение","confirm-apply":"Подтвердить","confirm-title":"Подтвердите изменение","create-child-action":"Добавить вложенную","current-view-text":"Текущий","drop-files":"Отпустите, чтобы загрузить файлы сюда","empty-text":"Нет данных","layout-label":"Представление данных","move-action":"Переместить…","move-apply":"Переместить","move-first":"Первым","move-last":"Последним","move-no-targets":"Эту запись некуда переместить.","move-place":"Место","move-root":"На верхний уровень","move-title":"Переместить «{label}»","notice-action-not-allowed":"Это действие недоступно для выбранной записи.","notice-capability-not-provided":"Хост не поддерживает это действие.","notice-confirm-required":"Действие нужно сначала подтвердить.","notice-conflict":"Данные изменились. Обновите представление и повторите действие.","notice-failed":"Действие не выполнено.","notice-fetch-failed":"Не удалось получить данные. Повторите попытку.","notice-forbidden":"Недостаточно прав для этого действия.","notice-load-failed":"Не удалось загрузить данные.","notice-no-base-revision":"Настройки пока нельзя сохранить: у представления нет их текущей версии. Обновите представление.","notice-not-confirmed":"Действие отменено.","notice-port-not-connected":"Источник данных не подключён.","notice-record-invalid":"Запись не найдена.","notice-refused":"Действие отклонено.","notice-saved-view-levels":"Общие виды требуют более новой версии порта.","notice-stale-revision":"Данные успели изменить. Обновите представление и повторите действие.","notice-unavailable":"Действие недоступно.","of-total":"{shown} из {total}","records-label":"Записи","show-all":"Показать все","state-load-failed":"Не удалось загрузить данные","state-loading":"Загрузка данных","state-port-not-connected":"Порт данных не подключён","state-query-failed":"Запрос не выполнен","state-unavailable":"Данные временно недоступны","unnamed-action":"Действие"}}');
3780
+
3781
+ /***/ }
3782
+
3783
+ /******/ });
3784
+ /************************************************************************/
3785
+ /******/ // The module cache
3786
+ /******/ const __webpack_module_cache__ = {};
3787
+ /******/
3788
+ /******/ // The require function
3789
+ /******/ function __webpack_require__(moduleId) {
3790
+ /******/ // Check if module is in cache
3791
+ /******/ const cachedModule = __webpack_module_cache__[moduleId];
3792
+ /******/ if (cachedModule !== undefined) {
3793
+ /******/ return cachedModule.exports;
3794
+ /******/ }
3795
+ /******/ // Create a new module (and put it into the cache)
3796
+ /******/ const module = __webpack_module_cache__[moduleId] = {
3797
+ /******/ // no module.id needed
3798
+ /******/ // no module.loaded needed
3799
+ /******/ exports: {}
3800
+ /******/ };
3801
+ /******/
3802
+ /******/ // Execute the module function
3803
+ /******/ if (!(moduleId in __webpack_modules__)) {
3804
+ /******/ delete __webpack_module_cache__[moduleId];
3805
+ /******/ const e = new Error("Cannot find module '" + moduleId + "'");
3806
+ /******/ e.code = 'MODULE_NOT_FOUND';
3807
+ /******/ throw e;
3808
+ /******/ }
3809
+ /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
3810
+ /******/
3811
+ /******/ // Return the exports of the module
3812
+ /******/ return module.exports;
3813
+ /******/ }
3814
+ /******/
3815
+ /************************************************************************/
3816
+ /******/ /* webpack/runtime/async module */
3817
+ /******/ (() => {
3818
+ /******/ const hasSymbol = typeof Symbol === "function";
3819
+ /******/ const webpackQueues = hasSymbol ? Symbol("webpack queues") : "__webpack_queues__";
3820
+ /******/ const webpackExports = hasSymbol ? Symbol("webpack exports") : "__webpack_exports__";
3821
+ /******/ const webpackError = hasSymbol ? Symbol("webpack error") : "__webpack_error__";
3822
+ /******/
3823
+ /******/ const resolveQueue = (queue) => {
3824
+ /******/ if(queue?.d < 1) {
3825
+ /******/ queue.d = 1;
3826
+ /******/ queue.forEach((fn) => (fn.r--));
3827
+ /******/ queue.forEach((fn) => (fn.r-- ? fn.r++ : fn()));
3828
+ /******/ }
3829
+ /******/ }
3830
+ /******/ const wrapDeps = (deps) => (deps.map((dep) => {
3831
+ /******/ if(dep !== null && typeof dep === "object") {
3832
+ /******/
3833
+ /******/ if(dep[webpackQueues]) return dep;
3834
+ /******/ if(dep.then) {
3835
+ /******/ const queue = [];
3836
+ /******/ queue.d = 0;
3837
+ /******/ dep.then((r) => {
3838
+ /******/ obj[webpackExports] = r;
3839
+ /******/ resolveQueue(queue);
3840
+ /******/ }, (e) => {
3841
+ /******/ obj[webpackError] = e;
3842
+ /******/ resolveQueue(queue);
3843
+ /******/ });
3844
+ /******/ const obj = {};
3845
+ /******/
3846
+ /******/ obj[webpackQueues] = (fn) => (fn(queue));
3847
+ /******/ return obj;
3848
+ /******/ }
3849
+ /******/ }
3850
+ /******/ const ret = {};
3851
+ /******/ ret[webpackQueues] = x => {};
3852
+ /******/ ret[webpackExports] = dep;
3853
+ /******/ return ret;
3854
+ /******/ }));
3855
+ /******/ __webpack_require__.a = (module, body, hasAwait) => {
3856
+ /******/ let queue;
3857
+ /******/ hasAwait && ((queue = []).d = -1);
3858
+ /******/ const depQueues = new Set();
3859
+ /******/ const exports = module.exports;
3860
+ /******/ let currentDeps;
3861
+ /******/ let outerResolve;
3862
+ /******/ let reject;
3863
+ /******/ const promise = new Promise((resolve, rej) => {
3864
+ /******/ reject = rej;
3865
+ /******/ outerResolve = resolve;
3866
+ /******/ });
3867
+ /******/ promise[webpackExports] = exports;
3868
+ /******/ promise[webpackQueues] = (fn) => (queue && fn(queue), depQueues.forEach(fn), promise["catch"](x => {}));
3869
+ /******/ module.exports = promise;
3870
+ /******/ const handle = (deps) => {
3871
+ /******/ currentDeps = wrapDeps(deps);
3872
+ /******/ let fn;
3873
+ /******/ const getResult = () => (currentDeps.map((d) => {
3874
+ /******/
3875
+ /******/ if(d[webpackError]) throw d[webpackError];
3876
+ /******/ return d[webpackExports];
3877
+ /******/ }))
3878
+ /******/ const promise = new Promise((resolve) => {
3879
+ /******/ fn = () => (resolve(getResult));
3880
+ /******/ fn.r = 0;
3881
+ /******/ const fnQueue = (q) => (q !== queue && !depQueues.has(q) && (depQueues.add(q), q && !q.d && (fn.r++, q.push(fn))));
3882
+ /******/ currentDeps.map((dep) => (dep[webpackQueues](fnQueue)));
3883
+ /******/ });
3884
+ /******/ return fn.r ? promise : getResult();
3885
+ /******/ }
3886
+ /******/ const done = (err) => ((err ? reject(promise[webpackError] = err) : outerResolve(exports)), resolveQueue(queue))
3887
+ /******/ body(handle, done);
3888
+ /******/ queue?.d < 0 && (queue.d = 0);
3889
+ /******/ };
3890
+ /******/ })();
3891
+ /******/
3892
+ /******/ /* webpack/runtime/compat get default export */
3893
+ /******/ (() => {
3894
+ /******/ // getDefaultExport function for compatibility with non-harmony modules
3895
+ /******/ __webpack_require__.n = (module) => {
3896
+ /******/ const getter = module && module.__esModule ?
3897
+ /******/ () => (module['default']) :
3898
+ /******/ () => (module);
3899
+ /******/ __webpack_require__.d(getter, { a: getter });
3900
+ /******/ return getter;
3901
+ /******/ };
3902
+ /******/ })();
3903
+ /******/
3904
+ /******/ /* webpack/runtime/define property getters */
3905
+ /******/ (() => {
3906
+ /******/ // define getter/value functions for harmony exports
3907
+ /******/ __webpack_require__.d = (exports, definition) => {
3908
+ /******/ if(Array.isArray(definition)) {
3909
+ /******/ var i = 0;
3910
+ /******/ while(i < definition.length) {
3911
+ /******/ var key = definition[i++];
3912
+ /******/ var binding = definition[i++];
3913
+ /******/ if(!__webpack_require__.o(exports, key)) {
3914
+ /******/ if(binding === 0) {
3915
+ /******/ Object.defineProperty(exports, key, { enumerable: true, value: definition[i++] });
3916
+ /******/ } else {
3917
+ /******/ Object.defineProperty(exports, key, { enumerable: true, get: binding });
3918
+ /******/ }
3919
+ /******/ } else if(binding === 0) { i++; }
3920
+ /******/ }
3921
+ /******/ } else {
3922
+ /******/ for(var key in definition) {
3923
+ /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
3924
+ /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
3925
+ /******/ }
3926
+ /******/ }
3927
+ /******/ }
3928
+ /******/ };
3929
+ /******/ })();
3930
+ /******/
3931
+ /******/ /* webpack/runtime/hasOwnProperty shorthand */
3932
+ /******/ (() => {
3933
+ /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
3934
+ /******/ })();
3935
+ /******/
3936
+ /******/ /* webpack/runtime/make namespace object */
3937
+ /******/ (() => {
3938
+ /******/ // define __esModule on exports
3939
+ /******/ __webpack_require__.r = (exports) => {
3940
+ /******/ if(Symbol.toStringTag) {
3941
+ /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
3942
+ /******/ }
3943
+ /******/ Object.defineProperty(exports, '__esModule', { value: true });
3944
+ /******/ };
3945
+ /******/ })();
3946
+ /******/
3947
+ /************************************************************************/
3948
+ /******/
3949
+ /******/ // startup
3950
+ /******/ // Load entry module and return exports
3951
+ /******/ // This entry module used 'module' so it can't be inlined
3952
+ /******/ let __webpack_exports__ = __webpack_require__("36b027867bab");
3953
+ /******/
3954
+ /******/ })()
3955
+ ;