@cube-nl/dmp-sdk 1.0.0-rc.3 → 1.1.0-rc.1

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 (89) hide show
  1. package/README.md +347 -25
  2. package/dist/app.d.ts +36 -2
  3. package/dist/app.js +192 -10
  4. package/dist/browser.js +6 -1
  5. package/dist/client.d.ts +2 -0
  6. package/dist/cube-sdk.js +179 -2
  7. package/dist/cube-ui.manifest.json +971 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.js +4 -0
  10. package/dist/preview.d.ts +18 -0
  11. package/dist/preview.js +199 -34
  12. package/dist/size.json +6 -0
  13. package/dist/ui/aggregate-block.d.ts +73 -0
  14. package/dist/ui/aggregate-block.js +138 -0
  15. package/dist/ui/attributes.d.ts +5 -0
  16. package/dist/ui/attributes.js +72 -0
  17. package/dist/ui/block.d.ts +63 -0
  18. package/dist/ui/block.js +229 -0
  19. package/dist/ui/blocks/app-shell.d.ts +4 -0
  20. package/dist/ui/blocks/app-shell.js +167 -0
  21. package/dist/ui/blocks/board.d.ts +8 -0
  22. package/dist/ui/blocks/board.js +292 -0
  23. package/dist/ui/blocks/calc.d.ts +9 -0
  24. package/dist/ui/blocks/calc.js +240 -0
  25. package/dist/ui/blocks/chart.d.ts +7 -0
  26. package/dist/ui/blocks/chart.js +166 -0
  27. package/dist/ui/blocks/detail.d.ts +8 -0
  28. package/dist/ui/blocks/detail.js +270 -0
  29. package/dist/ui/blocks/field.d.ts +8 -0
  30. package/dist/ui/blocks/field.js +16 -0
  31. package/dist/ui/blocks/form.d.ts +8 -0
  32. package/dist/ui/blocks/form.js +153 -0
  33. package/dist/ui/blocks/gap-list.d.ts +4 -0
  34. package/dist/ui/blocks/gap-list.js +36 -0
  35. package/dist/ui/blocks/kpi.d.ts +4 -0
  36. package/dist/ui/blocks/kpi.js +74 -0
  37. package/dist/ui/blocks/period-picker.d.ts +4 -0
  38. package/dist/ui/blocks/period-picker.js +155 -0
  39. package/dist/ui/blocks/state.d.ts +4 -0
  40. package/dist/ui/blocks/state.js +135 -0
  41. package/dist/ui/blocks/table.d.ts +8 -0
  42. package/dist/ui/blocks/table.js +607 -0
  43. package/dist/ui/blocks/top.d.ts +4 -0
  44. package/dist/ui/blocks/top.js +67 -0
  45. package/dist/ui/chart-svg.d.ts +28 -0
  46. package/dist/ui/chart-svg.js +111 -0
  47. package/dist/ui/csv.d.ts +9 -0
  48. package/dist/ui/csv.js +55 -0
  49. package/dist/ui/define.d.ts +24 -0
  50. package/dist/ui/define.js +107 -0
  51. package/dist/ui/dom.d.ts +12 -0
  52. package/dist/ui/dom.js +57 -0
  53. package/dist/ui/fields.d.ts +39 -0
  54. package/dist/ui/fields.js +73 -0
  55. package/dist/ui/format.d.ts +44 -0
  56. package/dist/ui/format.js +214 -0
  57. package/dist/ui/inputs.d.ts +35 -0
  58. package/dist/ui/inputs.js +161 -0
  59. package/dist/ui/manifest.d.ts +40 -0
  60. package/dist/ui/manifest.js +878 -0
  61. package/dist/ui/period.d.ts +18 -0
  62. package/dist/ui/period.js +52 -0
  63. package/dist/ui/query.d.ts +26 -0
  64. package/dist/ui/query.js +98 -0
  65. package/dist/ui/schema.d.ts +23 -0
  66. package/dist/ui/schema.js +21 -0
  67. package/dist/ui/styles.d.ts +8 -0
  68. package/dist/ui/styles.js +51 -0
  69. package/dist/ui/texts.d.ts +134 -0
  70. package/dist/ui/texts.js +122 -0
  71. package/dist/ui/theme.d.ts +58 -0
  72. package/dist/ui/theme.js +311 -0
  73. package/dist/ui/write.d.ts +20 -0
  74. package/dist/ui/write.js +54 -0
  75. package/dist/version.d.ts +1 -1
  76. package/dist/version.js +1 -1
  77. package/dist/wireframe.d.ts +37 -0
  78. package/dist/wireframe.js +315 -0
  79. package/fonts/OFL-albert-sans.txt +93 -0
  80. package/fonts/OFL-ibm-plex-sans.txt +93 -0
  81. package/fonts/OFL-inter.txt +93 -0
  82. package/fonts/OFL-nunito-sans.txt +93 -0
  83. package/fonts/OFL-source-sans-3.txt +93 -0
  84. package/fonts/albert-sans.woff2 +0 -0
  85. package/fonts/ibm-plex-sans.woff2 +0 -0
  86. package/fonts/inter.woff2 +0 -0
  87. package/fonts/nunito-sans.woff2 +0 -0
  88. package/fonts/source-sans-3.woff2 +0 -0
  89. package/package.json +8 -3
@@ -0,0 +1,311 @@
1
+ import { THEME_TEXTS } from './texts.js';
2
+ export const TOKEN_NAMES = [
3
+ 'color-primary',
4
+ 'color-primary-hover',
5
+ 'color-primary-subtle',
6
+ 'color-primary-text',
7
+ 'color-on-primary',
8
+ 'color-accent',
9
+ 'color-on-accent',
10
+ 'color-text',
11
+ 'color-text-muted',
12
+ 'color-bg',
13
+ 'color-surface',
14
+ 'color-border',
15
+ 'color-focus',
16
+ 'color-danger',
17
+ 'color-success',
18
+ 'color-warning',
19
+ 'color-example-bg',
20
+ 'color-example-text',
21
+ 'color-series-1',
22
+ 'color-series-2',
23
+ 'color-series-3',
24
+ 'color-series-4',
25
+ 'color-series-5',
26
+ 'color-series-6',
27
+ 'radius',
28
+ 'radius-small',
29
+ 'font-family',
30
+ ];
31
+ export const NEUTRAL_SOURCE = { primary_colour: '#475467', corners: 'normaal', font: 'system' };
32
+ export const FONTS = {
33
+ inter: {
34
+ family: 'Inter',
35
+ files: [{ file: 'inter.woff2', weight: '100 900', style: 'normal' }],
36
+ },
37
+ 'albert-sans': {
38
+ family: 'Albert Sans',
39
+ files: [{ file: 'albert-sans.woff2', weight: '100 900', style: 'normal' }],
40
+ },
41
+ 'source-sans-3': {
42
+ family: 'Source Sans 3',
43
+ files: [{ file: 'source-sans-3.woff2', weight: '200 900', style: 'normal' }],
44
+ },
45
+ 'nunito-sans': {
46
+ family: 'Nunito Sans',
47
+ files: [{ file: 'nunito-sans.woff2', weight: '200 1000', style: 'normal' }],
48
+ },
49
+ 'ibm-plex-sans': {
50
+ family: 'IBM Plex Sans',
51
+ files: [{ file: 'ibm-plex-sans.woff2', weight: '100 700', style: 'normal' }],
52
+ },
53
+ };
54
+ const FONT_FALLBACK = 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
55
+ const SURFACE = '#f6f8fa';
56
+ function parseHex(value) {
57
+ if (typeof value !== 'string')
58
+ return null;
59
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value);
60
+ if (!m || m[1] === undefined)
61
+ return null;
62
+ let h = m[1].toLowerCase();
63
+ if (h.length === 3)
64
+ h = [...h].map((c) => c + c).join('');
65
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
66
+ }
67
+ function toHex(rgb) {
68
+ return '#' + rgb.map((c) => Math.max(0, Math.min(255, c)).toString(16).padStart(2, '0')).join('');
69
+ }
70
+ function luminance(rgb) {
71
+ const [r, g, b] = rgb.map((c) => {
72
+ const s = c / 255;
73
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
74
+ });
75
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
76
+ }
77
+ export function contrastRatio(a, b) {
78
+ const ra = parseHex(a);
79
+ const rb = parseHex(b);
80
+ if (!ra || !rb)
81
+ return 1;
82
+ const la = luminance(ra);
83
+ const lb = luminance(rb);
84
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
85
+ }
86
+ function mix(a, b, t) {
87
+ const ra = parseHex(a);
88
+ const rb = parseHex(b);
89
+ return toHex(ra.map((c, i) => Math.round(c * (1 - t) + rb[i] * t)));
90
+ }
91
+ function on(c) {
92
+ return contrastRatio(c, '#ffffff') >= contrastRatio(c, '#000000') ? '#ffffff' : '#000000';
93
+ }
94
+ function rotateHue(hex, degrees) {
95
+ const [r, g, b] = parseHex(hex).map((c) => c / 255);
96
+ const max = Math.max(r, g, b);
97
+ const min = Math.min(r, g, b);
98
+ const l = (max + min) / 2;
99
+ const d = max - min;
100
+ let h = 0;
101
+ let s = 0;
102
+ if (d !== 0) {
103
+ s = d / (1 - Math.abs(2 * l - 1));
104
+ if (max === r)
105
+ h = ((g - b) / d) % 6;
106
+ else if (max === g)
107
+ h = (b - r) / d + 2;
108
+ else
109
+ h = (r - g) / d + 4;
110
+ h *= 60;
111
+ }
112
+ h = (((h + degrees) % 360) + 360) % 360;
113
+ const c = (1 - Math.abs(2 * l - 1)) * s;
114
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
115
+ const m = l - c / 2;
116
+ const sector = Math.floor(h / 60) % 6;
117
+ const [r1, g1, b1] = [
118
+ [c, x, 0],
119
+ [x, c, 0],
120
+ [0, c, x],
121
+ [0, x, c],
122
+ [x, 0, c],
123
+ [c, 0, x],
124
+ ][sector];
125
+ return toHex([r1, g1, b1].map((v) => Math.round((v + m) * 255)));
126
+ }
127
+ function normalise(value) {
128
+ const rgb = parseHex(value);
129
+ return rgb ? toHex(rgb) : null;
130
+ }
131
+ export function deriveTheme(source) {
132
+ const warnings = [];
133
+ let p = normalise(source.primary_colour);
134
+ if (p === null) {
135
+ p = normalise(NEUTRAL_SOURCE.primary_colour);
136
+ warnings.push({ code: 'invalid-primary', message: THEME_TEXTS.invalidPrimary });
137
+ }
138
+ let accent = null;
139
+ if (source.accent_colour !== undefined && source.accent_colour !== null) {
140
+ accent = normalise(source.accent_colour);
141
+ if (accent === null)
142
+ warnings.push({ code: 'invalid-accent', message: THEME_TEXTS.invalidAccent });
143
+ }
144
+ accent ??= rotateHue(p, 330);
145
+ if (contrastRatio(p, '#ffffff') < 3)
146
+ warnings.push({ code: 'primary-light', message: THEME_TEXTS.primaryLight });
147
+ // Primary text sits on the surface and on the subtle primary (secondary-button hover, badges): AA on both.
148
+ const subtle = mix(p, '#ffffff', 0.9);
149
+ let primaryText = p;
150
+ for (let i = 0; i <= 20; i++) {
151
+ primaryText = mix(p, '#000000', i / 20);
152
+ if (contrastRatio(primaryText, SURFACE) >= 4.5 && contrastRatio(primaryText, subtle) >= 4.5)
153
+ break;
154
+ }
155
+ if (primaryText !== p)
156
+ warnings.push({ code: 'primary-darkened-for-text', message: THEME_TEXTS.primaryDarkened });
157
+ const onPrimary = on(p);
158
+ const radii = {
159
+ scherp: ['0px', '0px'],
160
+ normaal: ['6px', '4px'],
161
+ rond: ['12px', '8px'],
162
+ };
163
+ const corners = typeof source.corners === 'string' && Object.hasOwn(radii, source.corners) ? source.corners : 'normaal';
164
+ const [radius, radiusSmall] = radii[corners];
165
+ const font = typeof source.font === 'string' && Object.hasOwn(FONTS, source.font) ? source.font : 'system';
166
+ const fontFamily = font === 'system' ? FONT_FALLBACK : `"${FONTS[font].family}", ${FONT_FALLBACK}`;
167
+ const tokens = {
168
+ 'color-primary': p,
169
+ 'color-primary-hover': mix(p, onPrimary === '#ffffff' ? '#000000' : '#ffffff', 0.15),
170
+ 'color-primary-subtle': subtle,
171
+ 'color-primary-text': primaryText,
172
+ 'color-on-primary': onPrimary,
173
+ 'color-accent': accent,
174
+ 'color-on-accent': on(accent),
175
+ 'color-text': '#1f2328',
176
+ 'color-text-muted': '#59636e',
177
+ 'color-bg': '#ffffff',
178
+ 'color-surface': SURFACE,
179
+ 'color-border': '#d1d9e0',
180
+ 'color-focus': primaryText,
181
+ 'color-danger': '#b42318',
182
+ 'color-success': '#067647',
183
+ 'color-warning': '#b54708',
184
+ 'color-example-bg': '#fef0c7',
185
+ 'color-example-text': '#7a2e0e',
186
+ 'color-series-1': p,
187
+ 'color-series-2': accent,
188
+ 'color-series-3': '#0e9384',
189
+ 'color-series-4': '#dc6803',
190
+ 'color-series-5': '#7a5af8',
191
+ 'color-series-6': '#475467',
192
+ radius,
193
+ 'radius-small': radiusSmall,
194
+ 'font-family': fontFamily,
195
+ };
196
+ return { tokens, warnings };
197
+ }
198
+ const COLOR_TOKEN = /^#[0-9a-f]{6}$/i;
199
+ const RADIUS_TOKEN = /^\d+(\.\d+)?(px|rem)$/;
200
+ const INLINE_IMAGE = /^data:image\/(svg\+xml|png);/;
201
+ // `/*` could swallow the rest of the stylesheet; the brief's list covers the other ways out of a declaration.
202
+ const UNSAFE_FONT_FAMILY = /[;{}<>\\\u0000-\u001f]|\/\*/;
203
+ const UNSAFE_URL = /["'()\\\s]/;
204
+ // A comma-separated list of quoted names and bare words: an unbalanced quote would run on through the stylesheet.
205
+ const FONT_FAMILY_ITEM = String.raw `(?:\s*"[^"]*"\s*|\s*'[^']*'\s*|[^"',]+)`;
206
+ const FONT_FAMILY_LIST = new RegExp(`^${FONT_FAMILY_ITEM}(?:,${FONT_FAMILY_ITEM})*$`);
207
+ function isRecord(value) {
208
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
209
+ }
210
+ function validToken(name, value) {
211
+ if (typeof value !== 'string')
212
+ return false;
213
+ if (name.startsWith('color-'))
214
+ return COLOR_TOKEN.test(value);
215
+ if (name.startsWith('radius'))
216
+ return RADIUS_TOKEN.test(value);
217
+ return (value.length > 0 && value.length <= 200 && !UNSAFE_FONT_FAMILY.test(value) && FONT_FAMILY_LIST.test(value));
218
+ }
219
+ function safeUrl(value, baseUrl, allowInline) {
220
+ if (typeof value !== 'string' || value === '')
221
+ return null;
222
+ if (allowInline && INLINE_IMAGE.test(value))
223
+ return UNSAFE_URL.test(value) ? null : value;
224
+ try {
225
+ const base = new URL(baseUrl);
226
+ const url = new URL(value, base);
227
+ const sameOrigin = (url.protocol === 'https:' || url.protocol === 'http:') && url.origin === base.origin;
228
+ if (sameOrigin && !UNSAFE_URL.test(url.href))
229
+ return url.href;
230
+ }
231
+ catch {
232
+ // An unparsable address is simply dropped.
233
+ }
234
+ return null;
235
+ }
236
+ function resolveFontFiles(files, baseUrl) {
237
+ const resolved = [];
238
+ for (const entry of Array.isArray(files) ? files : []) {
239
+ if (!isRecord(entry))
240
+ continue;
241
+ const src = safeUrl(entry.src, baseUrl, false);
242
+ if (src === null)
243
+ continue;
244
+ const weight = typeof entry.weight === 'string' && /^\d{1,4}( \d{1,4})?$/.test(entry.weight) ? entry.weight : '400';
245
+ resolved.push({ src, weight, style: entry.style === 'italic' ? 'italic' : 'normal' });
246
+ }
247
+ return resolved;
248
+ }
249
+ export function resolveTheme(input, baseUrl) {
250
+ const raw = input;
251
+ let source = NEUTRAL_SOURCE;
252
+ let tokenOverrides = null;
253
+ let fontFiles = null;
254
+ if (isRecord(raw)) {
255
+ if (Object.hasOwn(raw, 'source') && isRecord(raw.source)) {
256
+ source = raw.source;
257
+ tokenOverrides = Object.hasOwn(raw, 'tokens') ? raw.tokens : null;
258
+ fontFiles = Object.hasOwn(raw, 'font_files') ? raw.font_files : null;
259
+ }
260
+ else if (Object.hasOwn(raw, 'primary_colour')) {
261
+ source = raw;
262
+ }
263
+ }
264
+ const { tokens, warnings } = deriveTheme(source);
265
+ if (isRecord(tokenOverrides)) {
266
+ for (const name of TOKEN_NAMES) {
267
+ if (Object.hasOwn(tokenOverrides, name) && validToken(name, tokenOverrides[name])) {
268
+ tokens[name] = tokenOverrides[name];
269
+ }
270
+ }
271
+ }
272
+ const font = typeof source.font === 'string' && Object.hasOwn(FONTS, source.font)
273
+ ? source.font
274
+ : null;
275
+ let files = resolveFontFiles(fontFiles, baseUrl);
276
+ if (font !== null && files.length === 0 && !Array.isArray(fontFiles)) {
277
+ files = FONTS[font].files.flatMap((entry) => {
278
+ const src = safeUrl('cube-theme/' + entry.file, baseUrl, false);
279
+ return src === null ? [] : [{ src, weight: entry.weight, style: entry.style }];
280
+ });
281
+ }
282
+ return {
283
+ tokens,
284
+ logo: safeUrl(source.logo, baseUrl, true),
285
+ logoDark: safeUrl(source.logo_dark, baseUrl, true),
286
+ fontFamily: font === null ? null : FONTS[font].family,
287
+ fontFiles: files,
288
+ warnings,
289
+ };
290
+ }
291
+ export function themeCss(theme) {
292
+ // Zero specificity: an app's own `:root { --cube-... }` overrides a token although this sheet comes later.
293
+ return ':where(:root){' + TOKEN_NAMES.map((name) => `--cube-${name}:${theme.tokens[name]}`).join(';') + '}';
294
+ }
295
+ let current = null;
296
+ const listeners = new Set();
297
+ export function currentTheme() {
298
+ current ??= resolveTheme(null, 'http://localhost/');
299
+ return current;
300
+ }
301
+ export function setTheme(theme) {
302
+ current = theme;
303
+ for (const listener of [...listeners])
304
+ listener(theme);
305
+ }
306
+ export function onTheme(listener) {
307
+ listeners.add(listener);
308
+ return () => {
309
+ listeners.delete(listener);
310
+ };
311
+ }
@@ -0,0 +1,20 @@
1
+ import type { CubeAppSession } from '../app.js';
2
+ import { type Editor } from './inputs.js';
3
+ export declare const WRITE_CSS = "\nform{display:flex;flex-direction:column;gap:1rem}\n.actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1rem}\n.alert{margin:0 0 1rem;padding:.75rem 1rem;border:1px solid var(--cube-color-danger,#b42318);\nborder-radius:var(--cube-radius-small)}\n.alert p{margin:0}\n.alert ul{margin:.5rem 0 0;padding-inline-start:1.25rem}\n.alert button{margin-top:.5rem}\n.status{margin:.5rem 0 0}\n.status:empty{margin:0}\n";
4
+ /** One key per write attempt: kept only while the same body is sent again after the answer never came. */
5
+ export interface Attempt {
6
+ key: string;
7
+ body: string;
8
+ }
9
+ /** The attempt for `fingerprint`: the earlier one when it is the same body, else a new key. */
10
+ export declare function attemptFor(earlier: Attempt | null, fingerprint: string): Attempt;
11
+ /** Shows `message` (and `action`) in the alert above a form and moves focus there. */
12
+ export declare function showAlert(doc: Document, target: HTMLElement, message: string, action?: HTMLElement | null): void;
13
+ /**
14
+ * Shows why a write failed: a 422 on the fields it names, with the form-level line, and the messages for fields
15
+ * without an editor listed in the alert (`label` names them); anything else as one line. Never the browser's own
16
+ * (English) text of a network failure: stateFor would show a TypeError's message.
17
+ */
18
+ export declare function showWriteFailure(doc: Document, alert: HTMLElement, error: unknown, editors: Editor[], label: (field: string) => string): void;
19
+ /** Who is signed in, as a key: null when nobody is. A write must not go out as somebody who signed in since. */
20
+ export declare function userOf(session: CubeAppSession | undefined): string | null;
@@ -0,0 +1,54 @@
1
+ // What the blocks that write (cube-detail, cube-form, cube-board) share: the form frame, the alert above it, the
2
+ // message for a failed write, the idempotency key per attempt and who a write goes out as.
3
+ import { newIdempotencyKey } from '../client.js';
4
+ import { stateFor } from './block.js';
5
+ import { clear, h } from './dom.js';
6
+ import { fieldErrors } from './inputs.js';
7
+ import { NL } from './texts.js';
8
+ export const WRITE_CSS = `
9
+ form{display:flex;flex-direction:column;gap:1rem}
10
+ .actions{display:flex;flex-wrap:wrap;gap:.5rem;margin-top:1rem}
11
+ .alert{margin:0 0 1rem;padding:.75rem 1rem;border:1px solid var(--cube-color-danger,#b42318);
12
+ border-radius:var(--cube-radius-small)}
13
+ .alert p{margin:0}
14
+ .alert ul{margin:.5rem 0 0;padding-inline-start:1.25rem}
15
+ .alert button{margin-top:.5rem}
16
+ .status{margin:.5rem 0 0}
17
+ .status:empty{margin:0}
18
+ `;
19
+ /** The attempt for `fingerprint`: the earlier one when it is the same body, else a new key. */
20
+ export function attemptFor(earlier, fingerprint) {
21
+ return earlier?.body === fingerprint ? earlier : { key: newIdempotencyKey(), body: fingerprint };
22
+ }
23
+ /** Shows `message` (and `action`) in the alert above a form and moves focus there. */
24
+ export function showAlert(doc, target, message, action) {
25
+ clear(target);
26
+ target.append(h(doc, 'p', {}, message), ...(action === undefined || action === null ? [] : [action]));
27
+ target.hidden = false;
28
+ target.focus();
29
+ }
30
+ /**
31
+ * Shows why a write failed: a 422 on the fields it names, with the form-level line, and the messages for fields
32
+ * without an editor listed in the alert (`label` names them); anything else as one line. Never the browser's own
33
+ * (English) text of a network failure: stateFor would show a TypeError's message.
34
+ */
35
+ export function showWriteFailure(doc, alert, error, editors, label) {
36
+ const errors = fieldErrors(error);
37
+ if (errors !== null) {
38
+ for (const editor of editors)
39
+ editor.setError(errors[editor.name] ?? null);
40
+ const rest = Object.entries(errors).filter(([field]) => !editors.some((editor) => editor.name === field));
41
+ const list = rest.length === 0
42
+ ? null
43
+ : h(doc, 'ul', {}, ...rest.map(([field, message]) => h(doc, 'li', {}, `${label(field)}: ${message}`)));
44
+ showAlert(doc, alert, NL.formInvalid, list);
45
+ return;
46
+ }
47
+ const kind = stateFor(error).kind;
48
+ showAlert(doc, alert, kind === 'forbidden' ? NL.forbidden : kind === 'signed-out' ? NL.signedOut : NL.saveFailed);
49
+ }
50
+ /** Who is signed in, as a key: null when nobody is. A write must not go out as somebody who signed in since. */
51
+ export function userOf(session) {
52
+ const user = session?.user() ?? null;
53
+ return user === null ? null : String(user.id);
54
+ }
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This package's version. A test holds it equal to package.json's. */
2
- export declare const VERSION = "1.0.0-rc.3";
2
+ export declare const VERSION = "1.1.0-rc.1";
3
3
  /** The `"sdk"` value a `cube-app.json` manifest names for the API `CubeApp` offers here. */
4
4
  export declare const MANIFEST_SDK = "1";
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This package's version. A test holds it equal to package.json's. */
2
- export const VERSION = '1.0.0-rc.3';
2
+ export const VERSION = '1.1.0-rc.1';
3
3
  /** The `"sdk"` value a `cube-app.json` manifest names for the API `CubeApp` offers here. */
4
4
  export const MANIFEST_SDK = '1';
@@ -0,0 +1,37 @@
1
+ import type { ResourceClient } from './client.js';
2
+ import { PreviewStore } from './preview.js';
3
+ import type { DmpRecord } from './types.js';
4
+ import { type Format } from './ui/format.js';
5
+ import type { ResourceSchema } from './ui/schema.js';
6
+ export type GapType = 'tekst' | 'getal' | 'bedrag' | 'datum' | 'keuze' | 'ja/nee';
7
+ /** One entry of `ontbreekt[]` in cube-app.json: a field (or a whole resource) the DMP does not have yet. */
8
+ export interface GapEntry {
9
+ resource: string;
10
+ veld?: string;
11
+ type: GapType;
12
+ waarden?: string[];
13
+ scherm: string;
14
+ reden: string;
15
+ }
16
+ /** The valid gap entries of `ontbreekt`; malformed ones are dropped with a single warning. */
17
+ export declare function readGaps(value: unknown): GapEntry[];
18
+ export declare function gapFormat(type: GapType): Format;
19
+ /**
20
+ * A schema with the missing fields of its resource added, so a block that takes its columns from the schema shows
21
+ * them, and can filter and sort on them (the page only). They are writable: a wireframe's writes stay in the browser.
22
+ */
23
+ export declare function withGaps(schema: ResourceSchema | null, gaps: readonly GapEntry[]): ResourceSchema | null;
24
+ /** An example value for a missing field of row `id`: from mock rows that carry the field, else made up by type. */
25
+ export declare function inventValue(gap: GapEntry, id: unknown, mockRows: readonly DmpRecord[]): unknown;
26
+ /**
27
+ * A live resource some of whose fields do not exist yet. Those fields never reach the server (naming one would be a
28
+ * 422): each row gets an invented value, a filter or sort on one applies to the returned page only, and an aggregate
29
+ * that names one is answered from the mock rows.
30
+ */
31
+ export declare function mixedResource<T extends DmpRecord>(live: ResourceClient<T>, gaps: readonly GapEntry[], mock: PreviewStore, mockRows: readonly DmpRecord[]): ResourceClient<T>;
32
+ /**
33
+ * A wireframe's writes, answered in this browser and never sent: what is created, changed or deleted lies over the
34
+ * wrapped client's reads until the page reloads. A created row's id is `voorbeeld-<n>`. A list shows a created or
35
+ * changed row only where the query's filter puts it, so a moved board card lands in its new column.
36
+ */
37
+ export declare function simulatedWrites<T extends DmpRecord>(client: ResourceClient<T>, name: string): ResourceClient<T>;