@zeyos/zx 4.1.1 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/DESIGN-SYSTEM.md +33 -3
  3. package/README.md +22 -5
  4. package/dist/types/components/app-icon/app-icon.d.ts +8 -1
  5. package/dist/types/components/aurora/aurora.d.ts +72 -0
  6. package/dist/types/components/calendar/calendar-model.d.ts +202 -0
  7. package/dist/types/components/calendar/calendar.d.ts +517 -0
  8. package/dist/types/components/card-view/card-view.d.ts +303 -0
  9. package/dist/types/components/card-view/record-card.d.ts +242 -0
  10. package/dist/types/components/dialog/dialog.d.ts +6 -1
  11. package/dist/types/components/dropdown/dropdown.d.ts +5 -0
  12. package/dist/types/components/kanban-view/kanban-view.d.ts +572 -0
  13. package/dist/types/components/launcher/launcher.d.ts +5 -0
  14. package/dist/types/components/message/message.d.ts +9 -1
  15. package/dist/types/components/modal/modal.d.ts +44 -7
  16. package/dist/types/components/sheet/sheet.d.ts +22 -12
  17. package/dist/types/components/table/table.d.ts +120 -31
  18. package/dist/types/components/table-view/table-view.d.ts +192 -0
  19. package/dist/types/components/view/record-view.d.ts +464 -0
  20. package/dist/types/core/overlay-host.d.ts +12 -0
  21. package/dist/types/index.d.ts +7 -0
  22. package/dist/types/zeyos/calendar.d.ts +91 -0
  23. package/dist/types/zeyos/icons.d.ts +18 -2
  24. package/dist/types/zeyos/index.d.ts +4 -0
  25. package/dist/types/zeyos/legacy-saved-views.d.ts +66 -0
  26. package/dist/types/zeyos/saved-views.d.ts +331 -0
  27. package/dist/types/zeyos/view.d.ts +305 -0
  28. package/dist/zx-zeyos.esm.js +4742 -1470
  29. package/dist/zx-zeyos.esm.min.js +1 -1
  30. package/dist/zx.css +2536 -471
  31. package/dist/zx.esm.js +5193 -223
  32. package/dist/zx.esm.min.js +3 -3
  33. package/dist/zx.global.js +5193 -223
  34. package/dist/zx.global.min.js +3 -3
  35. package/dist/zx.min.css +1 -1
  36. package/docs/api.json +1729 -182
  37. package/docs/llms.md +236 -32
  38. package/docs/llms.txt +16 -6
  39. package/package.json +1 -1
  40. package/src/components/account-menu/account-menu.css +1 -1
  41. package/src/components/app-icon/app-icon.css +77 -51
  42. package/src/components/app-icon/app-icon.js +28 -4
  43. package/src/components/app-rail/app-rail.css +12 -3
  44. package/src/components/app-sidebar/app-sidebar.css +11 -2
  45. package/src/components/aurora/aurora.css +143 -0
  46. package/src/components/aurora/aurora.js +146 -0
  47. package/src/components/avatar/avatar.css +1 -1
  48. package/src/components/button/button.css +2 -2
  49. package/src/components/calendar/calendar-model.js +402 -0
  50. package/src/components/calendar/calendar.css +609 -0
  51. package/src/components/calendar/calendar.js +1619 -0
  52. package/src/components/card/card.css +2 -2
  53. package/src/components/card-view/card-view.css +389 -0
  54. package/src/components/card-view/card-view.js +527 -0
  55. package/src/components/card-view/record-card.js +392 -0
  56. package/src/components/context-menu/context-menu.css +4 -4
  57. package/src/components/context-menu/context-menu.js +5 -1
  58. package/src/components/date-picker/date-picker.css +31 -12
  59. package/src/components/datebox/date-range-box.css +17 -2
  60. package/src/components/datebox/datebox.css +18 -3
  61. package/src/components/dialog/dialog.css +4 -3
  62. package/src/components/dialog/dialog.js +2 -1
  63. package/src/components/dropdown/dropdown.css +17 -3
  64. package/src/components/dropdown/dropdown.js +7 -2
  65. package/src/components/kanban-view/kanban-view.css +216 -0
  66. package/src/components/kanban-view/kanban-view.js +1228 -0
  67. package/src/components/launcher/launcher.css +24 -9
  68. package/src/components/launcher/launcher.js +9 -2
  69. package/src/components/master-panel/master-panel.css +14 -3
  70. package/src/components/menu-button/menu-button.css +4 -4
  71. package/src/components/message/message.css +13 -21
  72. package/src/components/message/message.js +17 -11
  73. package/src/components/modal/modal.css +19 -5
  74. package/src/components/modal/modal.js +99 -25
  75. package/src/components/navigation-bar/navigation-bar.css +17 -5
  76. package/src/components/panel/panel.css +10 -2
  77. package/src/components/select/select.css +26 -8
  78. package/src/components/sheet/sheet.css +28 -7
  79. package/src/components/sheet/sheet.js +43 -23
  80. package/src/components/table/table.css +83 -5
  81. package/src/components/table/table.js +322 -37
  82. package/src/components/table-view/table-view.css +18 -0
  83. package/src/components/table-view/table-view.js +347 -0
  84. package/src/components/tag-picker/tag-picker.css +18 -4
  85. package/src/components/tooltip/tooltip.css +17 -3
  86. package/src/components/tooltip/tooltip.js +4 -1
  87. package/src/components/view/record-view.css +114 -0
  88. package/src/components/view/record-view.js +770 -0
  89. package/src/core/overlay-host.js +23 -0
  90. package/src/index.js +15 -0
  91. package/src/zeyos/calendar.js +115 -0
  92. package/src/zeyos/icons.js +11 -4
  93. package/src/zeyos/index.js +13 -0
  94. package/src/zeyos/launcher.js +2 -2
  95. package/src/zeyos/legacy-saved-views.js +155 -0
  96. package/src/zeyos/saved-views.js +815 -0
  97. package/src/zeyos/table.js +3 -1
  98. package/src/zeyos/view.js +543 -0
  99. package/styles/icons.css +3 -10
  100. package/styles/tokens/dark.css +4 -0
  101. package/styles/tokens/semantic.css +142 -3
  102. package/styles/zx.css +6 -0
@@ -0,0 +1,23 @@
1
+ import { resolveElement } from './dom.js';
2
+
3
+ const SCOPE_SELECTOR = '.zx-scope, [data-zx-theme], [data-zx-density], [data-zx-preset]';
4
+
5
+ /**
6
+ * Finds the closest styling scope for a top-layer element.
7
+ *
8
+ * Popovers and dialogs escape clipping even when their DOM parent is inside an application shell,
9
+ * so keeping them below that parent preserves inherited theme, density, material, and product
10
+ * token overrides without sacrificing top-layer behaviour. The document body remains the safe
11
+ * fallback for unscoped applications.
12
+ *
13
+ * @param {Element|string|null} [source=null] Element or selector whose styling scope should own the overlay.
14
+ * @returns {Element}
15
+ */
16
+ export function overlayHost(source = null) {
17
+ const context = source == null
18
+ ? (document.activeElement instanceof Element ? document.activeElement : null)
19
+ : resolveElement(source);
20
+ const scope = context?.closest?.(SCOPE_SELECTOR) ?? null;
21
+ if (!scope || scope === document.documentElement || scope === document.body) return document.body;
22
+ return scope;
23
+ }
package/src/index.js CHANGED
@@ -89,6 +89,7 @@ export { Toolbar } from './components/toolbar/toolbar.js';
89
89
  export { Toggle } from './components/toggle/toggle.js';
90
90
  export { Groupbox } from './components/groupbox/groupbox.js';
91
91
  export { Card } from './components/card/card.js';
92
+ export { Aurora } from './components/aurora/aurora.js';
92
93
  export { Panel } from './components/panel/panel.js';
93
94
  export { MasterPanel } from './components/master-panel/master-panel.js';
94
95
  export {
@@ -133,7 +134,21 @@ export {
133
134
  DateRangeBox, formatRangeText, parseRangeText
134
135
  } from './components/datebox/date-range-box.js';
135
136
  export { Timebox, splitTime, joinTime } from './components/timebox/timebox.js';
137
+ export { Calendar } from './components/calendar/calendar.js';
138
+ export {
139
+ CALENDAR_VIEWS, addCalendarDays, calendarDayDifference, calendarDayKey,
140
+ calendarEventIntersects, calendarEventSpansDays, calendarRange, cloneCalendarEvent,
141
+ layoutCalendarSpans, layoutTimedCalendarEvents, normalizeCalendarEvents,
142
+ shiftCalendarEvent, startOfCalendarDay, startOfCalendarWeek
143
+ } from './components/calendar/calendar-model.js';
144
+ export {
145
+ RecordView, normalizeViewFields, reconcileFieldOrder, moveViewField,
146
+ readViewField, renderViewField, sortViewRecords, normalizeViewState, viewRecordId
147
+ } from './components/view/record-view.js';
136
148
  export { Table, reorderTableRows } from './components/table/table.js';
149
+ export { TableView } from './components/table-view/table-view.js';
150
+ export { CardView } from './components/card-view/card-view.js';
151
+ export { KanbanView } from './components/kanban-view/kanban-view.js';
137
152
  export { Grid, billingColumns, isBillingLine } from './components/grid/grid.js';
138
153
  export {
139
154
  Chart, ChartJsAdapter, chartSummaryTable, cloneChartValue, isEmptyChartData
@@ -0,0 +1,115 @@
1
+ import { Calendar, calendarDayDifference } from '../index.js';
2
+ import { dateToUnixSeconds, unixSecondsToDate } from './query.js';
3
+
4
+ /**
5
+ * @typedef {Object} ZeyosCalendarOptions
6
+ * @property {Record<string, any>[]} [appointments=[]] Current ZeyOS appointment/event occurrences.
7
+ * @property {Record<string, any>[]} [events] Already-normalized generic Calendar records.
8
+ * @property {string|number|Date|null} [date] Calendar anchor.
9
+ * @property {'agenda'|'day'|'week'|'month'|'year'} [view='month'] Initial view.
10
+ * @property {boolean} [optimistic=true] Apply move/resize proposals locally before persistence.
11
+ * @property {boolean} [editable=false] Enable moving and resizing.
12
+ * @property {number} [weekStart=1] User-preferred first weekday.
13
+ * @property {boolean} [workweek=false] Show five days in week view.
14
+ * @property {(event: CustomEvent<Record<string, any>>) => void} [oneventchange] Persistence listener.
15
+ */
16
+
17
+ /**
18
+ * Maps one expanded ZeyOS appointment/event row to the product-neutral Calendar shape. Occurrence
19
+ * identity includes the start timestamp because recurring rows intentionally share their base ID;
20
+ * persistence callers keep using `event.data.ID`.
21
+ * @param {Record<string, any>} record ZeyOS row.
22
+ * @returns {Record<string, any>} Generic Calendar event.
23
+ */
24
+ export function zeyosAppointmentToEvent(record) {
25
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
26
+ throw new TypeError('ZeyOS appointment must be an object');
27
+ }
28
+ const baseId = record.ID ?? record.id;
29
+ if (baseId == null || baseId === '') throw new TypeError('ZeyOS appointment requires ID');
30
+ const start = unixSecondsToDate(record.datefrom);
31
+ const end = unixSecondsToDate(record.dateto ?? record.datefrom);
32
+ if (!(start instanceof Date) || !(end instanceof Date)) throw new TypeError('ZeyOS appointment requires datefrom and dateto');
33
+ const association = String(record.assoc_name ?? '').trim();
34
+ const name = String(record.name ?? '').trim() || 'Untitled appointment';
35
+ const title = record.entity && association ? `${association}: ${name}` : name;
36
+ const allDay = record.allDay != null ? Boolean(record.allDay)
37
+ : isMidnight(start) && isMidnight(end) && calendarDayDifference(start, end) >= 1;
38
+ return {
39
+ id: String(record.occurrenceid ?? `${baseId}@${record.datefrom}`),
40
+ title,
41
+ start,
42
+ end,
43
+ allDay,
44
+ color: zeyosCalendarColor(record.color),
45
+ location: String(record.location ?? '').trim(),
46
+ editable: record.editable == null ? null : Boolean(record.editable),
47
+ durationEditable: record.durationEditable == null ? null : Boolean(record.durationEditable),
48
+ data: record
49
+ };
50
+ }
51
+
52
+ /** @param {Record<string, any>[]} records @returns {Record<string, any>[]} */
53
+ export function zeyosAppointmentsToEvents(records) {
54
+ if (!Array.isArray(records)) throw new TypeError('ZeyOS appointments must be an array');
55
+ return records.map(zeyosAppointmentToEvent);
56
+ }
57
+
58
+ /**
59
+ * Converts a proposed Calendar occurrence back to the current ZeyOS appointment update fields.
60
+ * The base appointment identifier remains the source record's `ID`; date values become Unix
61
+ * seconds and the adapter never adds recurrence or DAV fields.
62
+ * @param {{start: Date, end: Date, data?: Record<string, any>}} event Calendar event proposal.
63
+ * @returns {{ID?: unknown, datefrom: number|null, dateto: number|null}}
64
+ */
65
+ export function calendarEventToZeyosPatch(event) {
66
+ if (!event || !(event.start instanceof Date) || !(event.end instanceof Date)) {
67
+ throw new TypeError('Calendar event patch requires start and end Dates');
68
+ }
69
+ const patch = {
70
+ datefrom: dateToUnixSeconds(event.start),
71
+ dateto: dateToUnixSeconds(event.end)
72
+ };
73
+ if (event.data?.ID != null) return { ID: event.data.ID, ...patch };
74
+ return patch;
75
+ }
76
+
77
+ /**
78
+ * Builds Calendar options while preserving every explicit generic option. `appointments` is
79
+ * mapped only when present; callers may instead pass already-normalized `events`.
80
+ * @param {ZeyosCalendarOptions & Record<string, any>} [options={}] ZeyOS and Calendar options.
81
+ * @returns {Record<string, any>} Plain Calendar options.
82
+ */
83
+ export function buildZeyosCalendarOptions(options = {}) {
84
+ const { appointments, ...calendarOptions } = options;
85
+ return {
86
+ weekStart: 1,
87
+ optimistic: true,
88
+ ...calendarOptions,
89
+ events: appointments == null
90
+ ? (Array.isArray(calendarOptions.events) ? calendarOptions.events : [])
91
+ : zeyosAppointmentsToEvents(appointments)
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Creates a Calendar that accepts current ZeyOS appointment/event occurrences directly.
97
+ * @param {Element|string|null} target Existing target or null.
98
+ * @param {ZeyosCalendarOptions & Record<string, any>} [options={}] Adapter and Calendar options.
99
+ * @returns {Calendar}
100
+ */
101
+ export function zeyosCalendar(target, options = {}) {
102
+ return new Calendar(target, buildZeyosCalendarOptions(options));
103
+ }
104
+
105
+ /** @param {unknown} value @returns {string|null} */
106
+ function zeyosCalendarColor(value) {
107
+ const color = String(value ?? '').trim();
108
+ if (!color) return null;
109
+ return /^[\da-f]{3,8}$/i.test(color) ? `#${color}` : color;
110
+ }
111
+
112
+ /** @param {Date} date @returns {boolean} */
113
+ function isMidnight(date) {
114
+ return date.getHours() === 0 && date.getMinutes() === 0 && date.getSeconds() === 0 && date.getMilliseconds() === 0;
115
+ }
@@ -59,6 +59,9 @@ export function moduleIcon(name, options = {}) {
59
59
  * @property {boolean} [standard=false] Use the stock Font Awesome fallback glyph.
60
60
  * @property {string|Node|(()=>string|Node)|null} [icon] Explicit glyph override, useful for an offline fallback.
61
61
  * @property {string} [color] Override the module colour for a server-defined fork or weblet.
62
+ * @property {string|number|null} [badge=null] Optional corner badge.
63
+ * @property {boolean} [selected=false] Whether the application identity is selected.
64
+ * @property {'tile'|'circle'} [shape='tile'] AppIcon vessel geometry.
62
65
  * @property {false|'subtle'|'strong'} [glass='subtle'] AppIcon material treatment.
63
66
  * @property {string|string[]} [class] Extra class names.
64
67
  */
@@ -66,7 +69,7 @@ export function moduleIcon(name, options = {}) {
66
69
  /**
67
70
  * Creates a module's icon chip: the glyph on the module's identity colour, the way ZeyOS draws
68
71
  * module icons in navigation and record headers. AppIcon identity glyphs stay white across every
69
- * module colour; material shading and the glyph shadow preserve their silhouette on light colours.
72
+ * module colour; material shading preserves their silhouette on light colours.
70
73
  * @param {string} name Module, entity, or resource name.
71
74
  * @param {ModuleChipOptions} [options={}] Display and accessibility options.
72
75
  * @returns {HTMLElement} A `<span class="zx-module-icon">` wrapping the glyph.
@@ -74,7 +77,7 @@ export function moduleIcon(name, options = {}) {
74
77
  export function moduleChip(name, options = {}) {
75
78
  const {
76
79
  size = 24, iconSize = '52%', label = null, title = false, standard = false,
77
- color = null
80
+ color = null, badge = null, selected = false
78
81
  } = options;
79
82
  const key = normalizeModuleName(name);
80
83
  const info = moduleInfo(name);
@@ -88,6 +91,9 @@ export function moduleChip(name, options = {}) {
88
91
  iconSize,
89
92
  color: background,
90
93
  label,
94
+ badge,
95
+ selected,
96
+ shape: options.shape ?? 'tile',
91
97
  glass: options.glass ?? 'subtle',
92
98
  class: ['zx-module-icon', options.class].flat().filter(Boolean)
93
99
  });
@@ -100,11 +106,12 @@ export function moduleChip(name, options = {}) {
100
106
  }
101
107
 
102
108
  /**
103
- * Creates the public ZeyOS AppIcon preset for any module, entity, fork, or weblet identity.
109
+ * Creates the public ZeyOS application-identity preset for any module, fork, or weblet. It uses
110
+ * the circular application vessel by default; use `moduleChip()` for compact entity/record tiles.
104
111
  * @param {string} name Module/entity identifier.
105
112
  * @param {ModuleChipOptions} [options={}] AppIcon options.
106
113
  * @returns {HTMLElement}
107
114
  */
108
115
  export function zeyosAppIcon(name, options = {}) {
109
- return moduleChip(name, options);
116
+ return moduleChip(name, { shape: 'circle', ...options });
110
117
  }
@@ -25,3 +25,16 @@ export {
25
25
  } from './select.js';
26
26
  export { zeyosForm } from './form.js';
27
27
  export { zeyosTable } from './table.js';
28
+ export {
29
+ buildZeyosCalendarOptions, calendarEventToZeyosPatch, zeyosAppointmentToEvent,
30
+ zeyosAppointmentsToEvents, zeyosCalendar
31
+ } from './calendar.js';
32
+ export { buildZeyosViewConfig, zeyosView } from './view.js';
33
+ export {
34
+ SAVED_VIEWS_VERSION, SavedViewDuplicateNameError, SavedViewRegistry,
35
+ SavedViewScopeMismatchError, createSavedViewRegistry, migrateSavedViewDocument,
36
+ normalizeSavedViewDocument, normalizeSavedViewScope
37
+ } from './saved-views.js';
38
+ export {
39
+ createLegacySavedViewTransport, legacySavedViewRequest
40
+ } from './legacy-saved-views.js';
@@ -1,5 +1,5 @@
1
1
  import { Launcher } from '../components/launcher/launcher.js';
2
- import { moduleChip } from './icons.js';
2
+ import { moduleChip, zeyosAppIcon } from './icons.js';
3
3
  import { moduleInfo } from './modules.js';
4
4
 
5
5
  /** Static application identifiers present in the current ZeyOS shell prototype. */
@@ -139,7 +139,7 @@ export function normalizeZeyosLauncherApplications(state = {}, options = {}) {
139
139
  ? options.renderApplicationIcon(app.raw, app)
140
140
  : app.type === 'weblet' && typeof options.renderWebletIcon === 'function'
141
141
  ? options.renderWebletIcon(app.raw, app) : null;
142
- return custom ?? moduleChip(app.module, {
142
+ return custom ?? zeyosAppIcon(app.module, {
143
143
  size: 36,
144
144
  standard: Boolean(options.standardIcons),
145
145
  color: app.color
@@ -0,0 +1,155 @@
1
+ /**
2
+ * @typedef {Object} SavedViewScope
3
+ * @property {string|number} userId Client identity used by the controller; never sent to the legacy endpoint.
4
+ * @property {string|number|null} workspaceId ZeyOS fork/workspace id.
5
+ * @property {string} resource ZeyOS resource whose record views are being saved.
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} LegacySavedViewTransportOptions
10
+ * @property {string} [recordName='__zx_record_views_v1__'] Reserved `userfields.name` value.
11
+ * @property {string} [viewPrefix='zx.record-views:'] Namespace prepended to `userfields.view`.
12
+ * @property {string} [module=''] Legacy UMI/module value.
13
+ */
14
+
15
+ /**
16
+ * Converts the callback form of `PG.load` into the promise request consumed by
17
+ * `createLegacySavedViewTransport`. Pass a bound method (`PG.load.bind(PG)`) so legacy receivers
18
+ * that depend on `this` remain intact.
19
+ * @param {(params:Record<string,unknown>,onLoad:(data:unknown)=>void,onCatch:(message:unknown,type?:unknown,info?:unknown)=>boolean)=>unknown} load Legacy loader.
20
+ * @returns {(params:Record<string,unknown>)=>Promise<unknown>}
21
+ */
22
+ export function legacySavedViewRequest(load) {
23
+ if (typeof load !== 'function') throw new TypeError('Legacy saved-view loader must be a function');
24
+ return (params) => new Promise((resolve, reject) => {
25
+ load(params, resolve, (message, type, info) => {
26
+ const error = new Error(String(message ?? 'ZeyOS saved-view request failed'));
27
+ Object.assign(error, { type, info });
28
+ reject(error);
29
+ return true;
30
+ });
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Adapts ZeyOS's existing `fields`, `fields_save`, and `fields_remove` endpoints to the atomic
36
+ * document transport used by saved record views. The endpoint derives the user from the
37
+ * authenticated session; accepting `userId` in the scope prevents accidental controller reuse but
38
+ * deliberately does not turn a client-supplied identity into an authorization input.
39
+ *
40
+ * `request` is injected because Zx supports both the legacy `PG.load` bridge and modern hosts. It
41
+ * receives the familiar plain endpoint parameters and returns a promise for the decoded response.
42
+ *
43
+ * @param {(params:Record<string,unknown>)=>Promise<unknown>} request Promise-based legacy request bridge.
44
+ * @param {LegacySavedViewTransportOptions} [options={}] Endpoint namespace options.
45
+ * @returns {{load:(scope:SavedViewScope)=>Promise<Record<string,unknown>|null>,save:(scope:SavedViewScope,document:Record<string,unknown>)=>Promise<Record<string,unknown>>,remove:(scope:SavedViewScope)=>Promise<void>}}
46
+ */
47
+ export function createLegacySavedViewTransport(request, options = {}) {
48
+ if (typeof request !== 'function') throw new TypeError('Saved-view request must be a function');
49
+ const recordName = normalizeNonEmpty(options.recordName, '__zx_record_views_v1__', 'recordName');
50
+ const viewPrefix = normalizeNonEmpty(options.viewPrefix, 'zx.record-views:', 'viewPrefix');
51
+ const module = options.module == null ? '' : String(options.module);
52
+
53
+ const paramsFor = (scope, page) => {
54
+ const normalized = normalizeScope(scope);
55
+ return {
56
+ umi: module,
57
+ page,
58
+ fork: normalized.workspaceId,
59
+ view: `${viewPrefix}${normalized.resource}`
60
+ };
61
+ };
62
+
63
+ return {
64
+ async load(scope) {
65
+ const response = await request(paramsFor(scope, 'fields'));
66
+ const value = findReservedDocument(response, recordName);
67
+ if (value == null || value === '') return null;
68
+ if (typeof value === 'string') {
69
+ try {
70
+ const parsed = JSON.parse(value);
71
+ return isRecord(parsed) ? parsed : null;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+ return isRecord(value) ? cloneJson(value) : null;
77
+ },
78
+
79
+ async save(scope, document) {
80
+ if (!isRecord(document)) throw new TypeError('Saved-view document must be an object');
81
+ const copy = cloneJson(document);
82
+ await request({
83
+ ...paramsFor(scope, 'fields_save'),
84
+ name: recordName,
85
+ data: JSON.stringify(copy)
86
+ });
87
+ return copy;
88
+ },
89
+
90
+ async remove(scope) {
91
+ await request({
92
+ ...paramsFor(scope, 'fields_remove'),
93
+ name: recordName
94
+ });
95
+ }
96
+ };
97
+ }
98
+
99
+ /** @param {unknown} scope @returns {{userId:string,workspaceId:string|number|null,resource:string}} */
100
+ function normalizeScope(scope) {
101
+ if (!isRecord(scope)) throw new TypeError('Saved-view scope must be an object');
102
+ if (scope.userId == null || String(scope.userId).trim() === '') {
103
+ throw new TypeError('Saved-view scope requires userId');
104
+ }
105
+ const resource = String(scope.resource ?? '').trim();
106
+ if (!resource) throw new TypeError('Saved-view scope requires resource');
107
+ const workspaceId = scope.workspaceId == null || scope.workspaceId === '' ? null : scope.workspaceId;
108
+ return { userId: String(scope.userId), workspaceId, resource };
109
+ }
110
+
111
+ /** @param {unknown} response @param {string} recordName @returns {unknown} */
112
+ function findReservedDocument(response, recordName) {
113
+ if (!Array.isArray(response) || !Array.isArray(response[0]) || !Array.isArray(response[1])) {
114
+ return null;
115
+ }
116
+ const index = response[0].findIndex((name) => String(name) === recordName);
117
+ return index < 0 ? null : response[1][index];
118
+ }
119
+
120
+ /** @param {unknown} value @param {string} fallback @param {string} label @returns {string} */
121
+ function normalizeNonEmpty(value, fallback, label) {
122
+ const normalized = value == null ? fallback : String(value).trim();
123
+ if (!normalized) throw new TypeError(`${label} must not be empty`);
124
+ return normalized;
125
+ }
126
+
127
+ /** @param {unknown} value @returns {value is Record<string,unknown>} */
128
+ function isRecord(value) {
129
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
130
+ }
131
+
132
+ /** @param {Record<string,unknown>} value @returns {Record<string,unknown>} */
133
+ function cloneJson(value) {
134
+ assertJsonValue(value, new Set());
135
+ try {
136
+ return JSON.parse(JSON.stringify(value));
137
+ } catch {
138
+ throw new TypeError('Saved-view document must be JSON-serializable');
139
+ }
140
+ }
141
+
142
+ /** @param {unknown} value @param {Set<object>} ancestors @returns {void} */
143
+ function assertJsonValue(value, ancestors) {
144
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
145
+ if (typeof value === 'number' && Number.isFinite(value)) return;
146
+ if (typeof value !== 'object') throw new TypeError('Saved-view document must be JSON-serializable');
147
+ if (ancestors.has(value)) throw new TypeError('Saved-view document must be JSON-serializable');
148
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
149
+ throw new TypeError('Saved-view document must be JSON-serializable');
150
+ }
151
+ ancestors.add(value);
152
+ const entries = Array.isArray(value) ? value : Object.values(value);
153
+ for (const entry of entries) assertJsonValue(entry, ancestors);
154
+ ancestors.delete(value);
155
+ }