@cube-nl/dmp-sdk 1.0.0-rc.4 → 1.1.0-rc.2

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 (96) hide show
  1. package/README.md +372 -9
  2. package/dist/app.d.ts +41 -4
  3. package/dist/app.js +252 -14
  4. package/dist/browser.js +14 -1
  5. package/dist/client.d.ts +2 -0
  6. package/dist/client.js +5 -1
  7. package/dist/cube-sdk.js +186 -2
  8. package/dist/cube-ui.manifest.json +971 -0
  9. package/dist/index.d.ts +6 -0
  10. package/dist/index.js +4 -0
  11. package/dist/preview.d.ts +11 -0
  12. package/dist/preview.js +60 -7
  13. package/dist/size.json +6 -0
  14. package/dist/trial.d.ts +32 -0
  15. package/dist/trial.js +55 -0
  16. package/dist/ui/aggregate-block.d.ts +73 -0
  17. package/dist/ui/aggregate-block.js +139 -0
  18. package/dist/ui/attributes.d.ts +5 -0
  19. package/dist/ui/attributes.js +72 -0
  20. package/dist/ui/author-error.d.ts +3 -0
  21. package/dist/ui/author-error.js +3 -0
  22. package/dist/ui/block.d.ts +69 -0
  23. package/dist/ui/block.js +238 -0
  24. package/dist/ui/blocks/app-shell.d.ts +4 -0
  25. package/dist/ui/blocks/app-shell.js +167 -0
  26. package/dist/ui/blocks/board.d.ts +8 -0
  27. package/dist/ui/blocks/board.js +333 -0
  28. package/dist/ui/blocks/calc.d.ts +9 -0
  29. package/dist/ui/blocks/calc.js +263 -0
  30. package/dist/ui/blocks/chart.d.ts +7 -0
  31. package/dist/ui/blocks/chart.js +166 -0
  32. package/dist/ui/blocks/detail.d.ts +8 -0
  33. package/dist/ui/blocks/detail.js +270 -0
  34. package/dist/ui/blocks/field.d.ts +8 -0
  35. package/dist/ui/blocks/field.js +16 -0
  36. package/dist/ui/blocks/form.d.ts +8 -0
  37. package/dist/ui/blocks/form.js +153 -0
  38. package/dist/ui/blocks/gap-list.d.ts +4 -0
  39. package/dist/ui/blocks/gap-list.js +36 -0
  40. package/dist/ui/blocks/kpi.d.ts +4 -0
  41. package/dist/ui/blocks/kpi.js +75 -0
  42. package/dist/ui/blocks/period-picker.d.ts +4 -0
  43. package/dist/ui/blocks/period-picker.js +155 -0
  44. package/dist/ui/blocks/state.d.ts +4 -0
  45. package/dist/ui/blocks/state.js +135 -0
  46. package/dist/ui/blocks/table.d.ts +8 -0
  47. package/dist/ui/blocks/table.js +666 -0
  48. package/dist/ui/blocks/top.d.ts +4 -0
  49. package/dist/ui/blocks/top.js +67 -0
  50. package/dist/ui/chart-svg.d.ts +28 -0
  51. package/dist/ui/chart-svg.js +111 -0
  52. package/dist/ui/csv.d.ts +9 -0
  53. package/dist/ui/csv.js +55 -0
  54. package/dist/ui/define.d.ts +24 -0
  55. package/dist/ui/define.js +107 -0
  56. package/dist/ui/dom.d.ts +12 -0
  57. package/dist/ui/dom.js +58 -0
  58. package/dist/ui/fields.d.ts +39 -0
  59. package/dist/ui/fields.js +73 -0
  60. package/dist/ui/format.d.ts +44 -0
  61. package/dist/ui/format.js +215 -0
  62. package/dist/ui/inputs.d.ts +35 -0
  63. package/dist/ui/inputs.js +161 -0
  64. package/dist/ui/manifest.d.ts +40 -0
  65. package/dist/ui/manifest.js +880 -0
  66. package/dist/ui/period.d.ts +18 -0
  67. package/dist/ui/period.js +52 -0
  68. package/dist/ui/query.d.ts +26 -0
  69. package/dist/ui/query.js +99 -0
  70. package/dist/ui/schema.d.ts +23 -0
  71. package/dist/ui/schema.js +21 -0
  72. package/dist/ui/styles.d.ts +8 -0
  73. package/dist/ui/styles.js +51 -0
  74. package/dist/ui/texts.d.ts +146 -0
  75. package/dist/ui/texts.js +138 -0
  76. package/dist/ui/theme.d.ts +58 -0
  77. package/dist/ui/theme.js +311 -0
  78. package/dist/ui/trial-banner.d.ts +25 -0
  79. package/dist/ui/trial-banner.js +180 -0
  80. package/dist/ui/write.d.ts +20 -0
  81. package/dist/ui/write.js +54 -0
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/dist/wireframe.d.ts +37 -0
  85. package/dist/wireframe.js +352 -0
  86. package/fonts/OFL-albert-sans.txt +93 -0
  87. package/fonts/OFL-ibm-plex-sans.txt +93 -0
  88. package/fonts/OFL-inter.txt +93 -0
  89. package/fonts/OFL-nunito-sans.txt +93 -0
  90. package/fonts/OFL-source-sans-3.txt +93 -0
  91. package/fonts/albert-sans.woff2 +0 -0
  92. package/fonts/ibm-plex-sans.woff2 +0 -0
  93. package/fonts/inter.woff2 +0 -0
  94. package/fonts/nunito-sans.woff2 +0 -0
  95. package/fonts/source-sans-3.woff2 +0 -0
  96. package/package.json +12 -4
@@ -0,0 +1,215 @@
1
+ import { AuthorError } from './author-error.js';
2
+ import { NL } from './texts.js';
3
+ /** The formats that hold a number: right-aligned, read and written as numbers. */
4
+ export const NUMERIC_FORMATS = ['number', 'integer', 'currency', 'percent'];
5
+ const LOCALE = 'nl-NL';
6
+ const TRUNCATIONS = ['day', 'week', 'month', 'quarter', 'year'];
7
+ const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
8
+ const NUMERIC = /^\s*-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?\s*$/i;
9
+ const MEASURE = /^(count|sum|avg|min|max)(?:\(([A-Za-z_][A-Za-z0-9_]*)\))?$/;
10
+ const DIMENSION = /^([A-Za-z_][A-Za-z0-9_]*)(?:\.([a-z]+))?$/;
11
+ const numberFormats = new Map();
12
+ const dateFormats = new Map();
13
+ function numberFormat(options) {
14
+ const key = JSON.stringify(options);
15
+ let format = numberFormats.get(key);
16
+ if (format === undefined) {
17
+ format = new Intl.NumberFormat(LOCALE, options);
18
+ numberFormats.set(key, format);
19
+ }
20
+ return format;
21
+ }
22
+ function dateFormat(options) {
23
+ const key = JSON.stringify(options);
24
+ let format = dateFormats.get(key);
25
+ if (format === undefined) {
26
+ format = new Intl.DateTimeFormat(LOCALE, options);
27
+ dateFormats.set(key, format);
28
+ }
29
+ return format;
30
+ }
31
+ /** A number, or a numeric string as the API may send a decimal (`"80.50"`); null for anything else. */
32
+ export function toNumber(value) {
33
+ if (typeof value === 'number')
34
+ return Number.isFinite(value) ? value : null;
35
+ if (typeof value === 'bigint')
36
+ return Number(value);
37
+ if (typeof value === 'string' && NUMERIC.test(value))
38
+ return Number(value);
39
+ return null;
40
+ }
41
+ function toDate(value) {
42
+ if (value instanceof Date)
43
+ return Number.isNaN(value.getTime()) ? null : value;
44
+ if (typeof value === 'number')
45
+ return Number.isFinite(value) ? new Date(value) : null;
46
+ if (typeof value === 'string')
47
+ return parseLocalDate(value);
48
+ return null;
49
+ }
50
+ /** Text for one value in the house format: nl-NL numbers, amounts and dates; `—` for nothing. */
51
+ export function formatValue(value, format, options = {}) {
52
+ if (value === null || value === undefined || value === '')
53
+ return NL.emptyValue;
54
+ switch (format) {
55
+ case 'number':
56
+ case 'integer':
57
+ case 'currency':
58
+ case 'percent': {
59
+ const n = toNumber(value);
60
+ if (n === null)
61
+ return String(value);
62
+ const compact = options.compact === true ? { notation: 'compact' } : {};
63
+ if (format === 'currency') {
64
+ return numberFormat({ style: 'currency', currency: options.currency ?? 'EUR', ...compact }).format(n);
65
+ }
66
+ if (format === 'percent')
67
+ return numberFormat({ style: 'percent', maximumFractionDigits: 1 }).format(n);
68
+ const digits = format === 'integer' ? 0 : options.compact === true ? 1 : 2;
69
+ return numberFormat({ maximumFractionDigits: digits, ...compact }).format(n);
70
+ }
71
+ case 'date':
72
+ case 'datetime': {
73
+ const date = toDate(value);
74
+ if (date === null)
75
+ return String(value);
76
+ const parts = { day: 'numeric', month: 'short', year: 'numeric' };
77
+ if (format === 'datetime')
78
+ Object.assign(parts, { hour: '2-digit', minute: '2-digit' });
79
+ // A bare date is a calendar day, not an instant: shifting it into another zone would change the day.
80
+ const bareDate = typeof value === 'string' && DATE_ONLY.test(value);
81
+ if (options.timeZone !== undefined && !bareDate)
82
+ parts.timeZone = options.timeZone;
83
+ return dateFormat(parts).format(date);
84
+ }
85
+ case 'boolean':
86
+ if (value === true || value === 'true' || value === 1)
87
+ return NL.yes;
88
+ if (value === false || value === 'false' || value === 0)
89
+ return NL.no;
90
+ return String(value);
91
+ default:
92
+ if (Array.isArray(value))
93
+ return value.map((item) => formatValue(item, 'text')).join(', ');
94
+ if (typeof value === 'object')
95
+ return JSON.stringify(value);
96
+ return String(value);
97
+ }
98
+ }
99
+ /**
100
+ * A label for an aggregate bucket key (`2026-Q2` → `K2 2026`). An unknown key comes back as is. `short` writes a
101
+ * month without its year (`nov`), for an axis.
102
+ */
103
+ export function formatBucket(key, truncation, options = {}) {
104
+ switch (truncation) {
105
+ case 'year':
106
+ return key;
107
+ case 'quarter': {
108
+ const m = /^(\d{4})-Q([1-4])$/.exec(key);
109
+ return m?.[1] !== undefined && m[2] !== undefined ? NL.quarter(m[2], m[1]) : key;
110
+ }
111
+ case 'week': {
112
+ const m = /^(\d{4})-W(\d{2})$/.exec(key);
113
+ return m?.[1] !== undefined && m[2] !== undefined ? NL.week(Number(m[2]), m[1]) : key;
114
+ }
115
+ case 'month': {
116
+ const m = /^(\d{4})-(\d{2})$/.exec(key);
117
+ if (m?.[1] === undefined || m[2] === undefined)
118
+ return key;
119
+ const month = localDate(Number(m[1]), Number(m[2]) - 1, 1);
120
+ return dateFormat(options.short === true ? { month: 'short' } : { month: 'short', year: 'numeric' })
121
+ .format(month);
122
+ }
123
+ case 'day': {
124
+ const date = DATE_ONLY.test(key) ? parseLocalDate(key) : null;
125
+ return date === null ? key : dateFormat({ day: 'numeric', month: 'short' }).format(date);
126
+ }
127
+ default:
128
+ return key;
129
+ }
130
+ }
131
+ /** `sum(total)` → `{ fn: 'sum', field: 'total' }`; a bare `count` counts rows. */
132
+ export function parseMeasure(measure) {
133
+ const m = MEASURE.exec(measure.trim());
134
+ const fn = m?.[1];
135
+ if (fn === undefined || (fn !== 'count' && m?.[2] === undefined)) {
136
+ throw new AuthorError(NL.invalid.measure(measure));
137
+ }
138
+ return { fn, field: m?.[2] ?? null };
139
+ }
140
+ /** `placed_on.month` → `{ field: 'placed_on', truncation: 'month' }`. */
141
+ export function parseDimension(by) {
142
+ const m = DIMENSION.exec(by.trim());
143
+ const field = m?.[1];
144
+ const truncation = m?.[2];
145
+ if (field === undefined || (truncation !== undefined && !TRUNCATIONS.includes(truncation))) {
146
+ throw new AuthorError(NL.invalid.dimension(by));
147
+ }
148
+ return { field, truncation: truncation ?? null };
149
+ }
150
+ /** The format a field's values get when the page does not say: from its schema type, else from a sample value. */
151
+ export function inferFormat(field, sample) {
152
+ if (field?.format === 'date')
153
+ return 'date';
154
+ if (field?.format === 'date-time')
155
+ return 'datetime';
156
+ if (field?.format === 'currency')
157
+ return 'currency';
158
+ switch (field?.type) {
159
+ case 'integer':
160
+ return 'integer';
161
+ case 'number':
162
+ return 'number';
163
+ case 'boolean':
164
+ return 'boolean';
165
+ case 'string':
166
+ // Live schemas carry no `format`, so a date column is only recognisable by its values.
167
+ return sampleDate(sample) ?? 'text';
168
+ }
169
+ // Unknown or union types (`string|integer`): the sample decides.
170
+ if (typeof sample === 'boolean')
171
+ return 'boolean';
172
+ if (typeof sample === 'number')
173
+ return Number.isInteger(sample) ? 'integer' : 'number';
174
+ return sampleDate(sample) ?? 'text';
175
+ }
176
+ function sampleDate(sample) {
177
+ if (typeof sample !== 'string')
178
+ return null;
179
+ if (DATE_ONLY.test(sample))
180
+ return parseLocalDate(sample) === null ? null : 'date';
181
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(sample) && !Number.isNaN(Date.parse(sample)))
182
+ return 'datetime';
183
+ return null;
184
+ }
185
+ /** A readable label from a field name: `placed_on` → `Placed on`, `customer_id` → `Customer`. */
186
+ export function humanize(field) {
187
+ const words = field
188
+ .replace(/_id$/, '')
189
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
190
+ .replace(/[_\-.]+/g, ' ')
191
+ .trim()
192
+ .toLowerCase();
193
+ return words === '' ? field : words.charAt(0).toUpperCase() + words.slice(1);
194
+ }
195
+ /** `YYYY-MM-DD` as local midnight (Date.parse would make it UTC midnight); anything else via Date.parse. */
196
+ export function parseLocalDate(value) {
197
+ const m = DATE_ONLY.exec(value);
198
+ if (m?.[1] !== undefined && m[2] !== undefined && m[3] !== undefined) {
199
+ const date = localDate(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
200
+ return date.getMonth() === Number(m[2]) - 1 ? date : null;
201
+ }
202
+ const time = Date.parse(value);
203
+ return Number.isNaN(time) ? null : new Date(time);
204
+ }
205
+ // `new Date(y, m, d)` maps years 0-99 to 1900-1999; setFullYear does not.
206
+ function localDate(year, month, day) {
207
+ const date = new Date(2000, 0, 1);
208
+ date.setFullYear(year, month, day);
209
+ return date;
210
+ }
211
+ /** The local calendar day as `YYYY-MM-DD`. */
212
+ export function isoDate(date) {
213
+ const pad = (n) => String(n).padStart(2, '0');
214
+ return `${String(date.getFullYear()).padStart(4, '0')}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
215
+ }
@@ -0,0 +1,35 @@
1
+ import { type Column } from './fields.js';
2
+ import { type Format } from './format.js';
3
+ /** One labelled input for one field. `read` checks and converts what was typed; `setError` shows a message. */
4
+ export interface Editor {
5
+ element: HTMLElement;
6
+ name: string;
7
+ read(): {
8
+ value: unknown;
9
+ } | {
10
+ error: string;
11
+ };
12
+ setError(message: string | null): void;
13
+ }
14
+ type Parsed = {
15
+ value: unknown;
16
+ } | {
17
+ error: string;
18
+ };
19
+ /** Styles for the editors, for the blocks that show them to include in their own `css`. */
20
+ export declare const EDITOR_CSS = "\n.field{display:flex;flex-direction:column;gap:.25rem;min-width:0}\n.field label{font-weight:600}\n.field .required{font-weight:400;color:var(--cube-color-text-muted)}\n.field.check{flex-direction:row;align-items:center;gap:.5rem}\n.field.check input{min-height:auto;width:1.25rem;height:1.25rem}\n.field [aria-invalid=true]{border-color:var(--cube-color-danger,#b42318)}\n.field-error{margin:0;color:var(--cube-color-danger,#b42318);font-size:.875rem}\n";
21
+ /**
22
+ * What a typed text means for a field of `format`: nothing typed is null; numbers are read the Dutch way
23
+ * (`12,5`, `1.234,5`) or the plain way (`12.5`); a percentage is typed as 21 and stored as 0.21.
24
+ */
25
+ export declare function parseInput(raw: string, format: Format): Parsed;
26
+ /** The first message per field of a 422 from the DMP, or null when the error is not one. */
27
+ export declare function fieldErrors(error: unknown): Record<string, string> | null;
28
+ /**
29
+ * The editor for one field: a text box, a number box that takes Dutch decimals, a date or date-time picker, a
30
+ * checkbox or a list of choices. `required` is announced with aria-required, not the native attribute, so the
31
+ * message the person reads is ours, in Dutch; the label shows " (verplicht)", hidden from the reader that already
32
+ * hears aria-required.
33
+ */
34
+ export declare function editorFor(doc: Document, column: Column, value: unknown, required: boolean): Editor;
35
+ export {};
@@ -0,0 +1,161 @@
1
+ import { isDmpError } from '../errors.js';
2
+ import { h } from './dom.js';
3
+ import { choicesFor } from './fields.js';
4
+ import { isoDate, NUMERIC_FORMATS } from './format.js';
5
+ import { NL } from './texts.js';
6
+ /** Styles for the editors, for the blocks that show them to include in their own `css`. */
7
+ export const EDITOR_CSS = `
8
+ .field{display:flex;flex-direction:column;gap:.25rem;min-width:0}
9
+ .field label{font-weight:600}
10
+ .field .required{font-weight:400;color:var(--cube-color-text-muted)}
11
+ .field.check{flex-direction:row;align-items:center;gap:.5rem}
12
+ .field.check input{min-height:auto;width:1.25rem;height:1.25rem}
13
+ .field [aria-invalid=true]{border-color:var(--cube-color-danger,#b42318)}
14
+ .field-error{margin:0;color:var(--cube-color-danger,#b42318);font-size:.875rem}
15
+ `;
16
+ // With a comma the dots can only group thousands: 1.234,5. Without one, a dot before exactly three digits groups
17
+ // thousands too (nl-NL input: 1.234 is 1234), unless the number starts with 0 (0.125); any other dot is a decimal.
18
+ const WITH_COMMA = /^-?(\d{1,3}(\.\d{3})+|\d*)(,\d+)?$/;
19
+ const GROUPED = /^-?[1-9]\d{0,2}(\.\d{3})+$/;
20
+ const PLAIN = /^-?(\d+\.?\d*|\.\d+)$/;
21
+ let nextId = 0;
22
+ function parseDutchNumber(text) {
23
+ if (text.includes(',')) {
24
+ return WITH_COMMA.test(text) && /\d/.test(text) ? Number(text.replace(/\./g, '').replace(',', '.')) : null;
25
+ }
26
+ if (GROUPED.test(text))
27
+ return Number(text.replace(/\./g, ''));
28
+ return PLAIN.test(text) ? Number(text) : null;
29
+ }
30
+ // toPrecision drops the binary noise of a division or multiplication by 100 (0.07 * 100 = 7.000000000000001).
31
+ const tidy = (n) => Number(n.toPrecision(12));
32
+ /**
33
+ * What a typed text means for a field of `format`: nothing typed is null; numbers are read the Dutch way
34
+ * (`12,5`, `1.234,5`) or the plain way (`12.5`); a percentage is typed as 21 and stored as 0.21.
35
+ */
36
+ export function parseInput(raw, format) {
37
+ if (raw.trim() === '')
38
+ return { value: null };
39
+ if (NUMERIC_FORMATS.includes(format)) {
40
+ const n = parseDutchNumber(raw.replace(/[\s €%]/g, ''));
41
+ if (n === null || !Number.isFinite(n))
42
+ return { error: NL.notANumber };
43
+ if (format === 'integer' && !Number.isInteger(n))
44
+ return { error: NL.notAnInteger };
45
+ return { value: format === 'percent' ? tidy(n / 100) : n };
46
+ }
47
+ if (format === 'boolean')
48
+ return { value: raw === 'true' || raw === '1' };
49
+ if (format === 'datetime') {
50
+ const time = Date.parse(raw);
51
+ return { value: Number.isNaN(time) ? raw : new Date(time).toISOString() };
52
+ }
53
+ return { value: raw };
54
+ }
55
+ /** The first message per field of a 422 from the DMP, or null when the error is not one. */
56
+ export function fieldErrors(error) {
57
+ if (!isDmpError(error) || error.status !== 422 || error.errors === null)
58
+ return null;
59
+ const result = {};
60
+ for (const [field, messages] of Object.entries(error.errors)) {
61
+ const first = Array.isArray(messages) ? messages.find((m) => typeof m === 'string') : undefined;
62
+ if (first !== undefined)
63
+ result[field] = first;
64
+ }
65
+ return Object.keys(result).length > 0 ? result : null;
66
+ }
67
+ /** A stored value as the text an input shows: `1234,5`, a percentage as 21, a date-time in local time. */
68
+ function inputText(value, format) {
69
+ if (value === null || value === undefined)
70
+ return '';
71
+ if (NUMERIC_FORMATS.includes(format)) {
72
+ const n = typeof value === 'number' ? value : Number(value);
73
+ if (typeof value === 'boolean' || !Number.isFinite(n))
74
+ return String(value);
75
+ return String(format === 'percent' ? tidy(n * 100) : n).replace('.', ',');
76
+ }
77
+ if (format === 'date' && typeof value === 'string')
78
+ return value.slice(0, 10);
79
+ if (format === 'datetime' && typeof value === 'string') {
80
+ const time = Date.parse(value);
81
+ if (Number.isNaN(time))
82
+ return value;
83
+ const date = new Date(time);
84
+ const pad = (n) => String(n).padStart(2, '0');
85
+ return `${isoDate(date)}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
86
+ }
87
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
88
+ }
89
+ function control(doc, column, value, id) {
90
+ const choices = choicesFor(column);
91
+ const text = inputText(value, column.format);
92
+ if (choices !== null) {
93
+ const known = choices.some((choice) => choice.value === text);
94
+ const list = text === '' || known ? choices : [...choices, { value: text, label: text }];
95
+ const select = h(doc, 'select', { id, name: column.name }, h(doc, 'option', { value: '' }, NL.emptyValue), ...list.map((choice) => h(doc, 'option', { value: choice.value }, choice.label)));
96
+ select.value = text;
97
+ return select;
98
+ }
99
+ if (column.format === 'text' && column.config?.multiline === true) {
100
+ const area = h(doc, 'textarea', { id, name: column.name, rows: '4' });
101
+ area.value = text;
102
+ return area;
103
+ }
104
+ const numeric = NUMERIC_FORMATS.includes(column.format);
105
+ const type = column.format === 'date' ? 'date' : column.format === 'datetime' ? 'datetime-local' : 'text';
106
+ const input = h(doc, 'input', {
107
+ id,
108
+ name: column.name,
109
+ type: column.format === 'boolean' ? 'checkbox' : type,
110
+ inputmode: numeric ? 'decimal' : null,
111
+ });
112
+ if (column.format === 'boolean') {
113
+ input.checked = value === true || value === 'true' || value === 1;
114
+ }
115
+ else {
116
+ input.value = text;
117
+ }
118
+ return input;
119
+ }
120
+ /**
121
+ * The editor for one field: a text box, a number box that takes Dutch decimals, a date or date-time picker, a
122
+ * checkbox or a list of choices. `required` is announced with aria-required, not the native attribute, so the
123
+ * message the person reads is ours, in Dutch; the label shows " (verplicht)", hidden from the reader that already
124
+ * hears aria-required.
125
+ */
126
+ export function editorFor(doc, column, value, required) {
127
+ const id = `cube-input-${++nextId}`;
128
+ const input = control(doc, column, value, id);
129
+ const checkbox = input.getAttribute('type') === 'checkbox';
130
+ if (required && !checkbox)
131
+ input.setAttribute('aria-required', 'true');
132
+ const mark = required && !checkbox
133
+ ? h(doc, 'span', { class: 'required', 'aria-hidden': 'true' }, NL.requiredMark)
134
+ : null;
135
+ const label = h(doc, 'label', { for: id }, column.label, mark);
136
+ const errorLine = h(doc, 'p', { id: `${id}-error`, class: 'field-error', hidden: true });
137
+ const element = h(doc, 'div', { class: checkbox ? 'field check' : 'field' }, ...(checkbox ? [input, label] : [label, input]), errorLine);
138
+ return {
139
+ element,
140
+ name: column.name,
141
+ read() {
142
+ if (checkbox)
143
+ return { value: input.checked };
144
+ if (required && input.value.trim() === '')
145
+ return { error: NL.required };
146
+ return parseInput(input.value, column.format);
147
+ },
148
+ setError(message) {
149
+ errorLine.textContent = message ?? '';
150
+ errorLine.hidden = message === null;
151
+ if (message === null) {
152
+ input.removeAttribute('aria-invalid');
153
+ input.removeAttribute('aria-describedby');
154
+ }
155
+ else {
156
+ input.setAttribute('aria-invalid', 'true');
157
+ input.setAttribute('aria-describedby', errorLine.id);
158
+ }
159
+ },
160
+ };
161
+ }
@@ -0,0 +1,40 @@
1
+ export type AttributeType = 'string' | 'boolean' | 'number' | 'enum' | 'resource' | 'field' | 'fields' | 'measure' | 'dimension' | 'selector' | 'filter' | 'date' | 'url' | 'id';
2
+ export interface AttributeSpec {
3
+ type: AttributeType;
4
+ required: boolean;
5
+ summary: string;
6
+ values?: readonly string[];
7
+ default?: string;
8
+ example?: string;
9
+ actions?: readonly string[];
10
+ }
11
+ export interface BlockSpec {
12
+ kind: 'block' | 'config';
13
+ since: string;
14
+ summary: string;
15
+ actions: readonly string[];
16
+ attributes: Readonly<Record<string, AttributeSpec>>;
17
+ children?: readonly string[];
18
+ events?: Readonly<Record<string, string>>;
19
+ properties?: Readonly<Record<string, string>>;
20
+ example: string;
21
+ }
22
+ export interface BlockManifest {
23
+ manifest: 1;
24
+ package: '@cube-nl/dmp-sdk';
25
+ version: string;
26
+ file: 'cube-sdk.js';
27
+ blocks: Record<string, BlockSpec>;
28
+ tokens: string[];
29
+ fonts: Record<string, {
30
+ family: string;
31
+ files: Array<{
32
+ file: string;
33
+ weight: string;
34
+ style: string;
35
+ }>;
36
+ }>;
37
+ }
38
+ /** What every element does, read by Claude to compose apps. One entry per tag, in English. */
39
+ export declare const BLOCK_SPECS: Readonly<Record<string, BlockSpec>>;
40
+ export declare function blockManifest(version: string): BlockManifest;