@cube-nl/dmp-sdk 1.0.0-rc.4 → 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 +298 -6
  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 +9 -0
  11. package/dist/preview.js +39 -1
  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,18 @@
1
+ export type PeriodKey = 'this-month' | '12m' | 'this-year' | 'custom';
2
+ /** ISO dates, both ends inclusive. */
3
+ export interface Period {
4
+ key: PeriodKey;
5
+ from: string;
6
+ to: string;
7
+ previous: {
8
+ from: string;
9
+ to: string;
10
+ };
11
+ /** The day the period was counted from (the picker's today); what lies after it has no data yet. */
12
+ today?: string;
13
+ }
14
+ /** The period the key names around `today`, with the one before it for comparison. `custom` needs its dates. */
15
+ export declare function resolvePeriod(key: PeriodKey, today: Date, custom?: {
16
+ from: string;
17
+ to: string;
18
+ }): Period;
@@ -0,0 +1,52 @@
1
+ import { isoDate, parseLocalDate } from './format.js';
2
+ const DAY_MS = 86_400_000;
3
+ // `new Date(y, m, d)` maps years 0-99 to 1900-1999; setFullYear does not.
4
+ function local(year, month, day) {
5
+ const date = new Date(2000, 0, 1);
6
+ date.setFullYear(year, month, day);
7
+ return date;
8
+ }
9
+ function range(from, to) {
10
+ return { from: isoDate(from), to: isoDate(to) };
11
+ }
12
+ /** First day of the month `offset` months from `today`'s, and the last day of the month `span - 1` months later. */
13
+ function months(today, offset, span) {
14
+ const year = today.getFullYear();
15
+ const month = today.getMonth() + offset;
16
+ return range(local(year, month, 1), local(year, month + span, 0));
17
+ }
18
+ function days(from, to) {
19
+ return Math.round((Date.UTC(to.getFullYear(), to.getMonth(), to.getDate()) -
20
+ Date.UTC(from.getFullYear(), from.getMonth(), from.getDate())) / DAY_MS) + 1;
21
+ }
22
+ /** The period the key names around `today`, with the one before it for comparison. `custom` needs its dates. */
23
+ export function resolvePeriod(key, today, custom) {
24
+ switch (key) {
25
+ case 'this-month':
26
+ return { key, ...months(today, 0, 1), previous: months(today, -1, 1) };
27
+ case '12m':
28
+ return { key, ...months(today, -11, 12), previous: months(today, -23, 12) };
29
+ case 'this-year': {
30
+ const year = today.getFullYear();
31
+ return {
32
+ key,
33
+ ...range(local(year, 0, 1), local(year, 11, 31)),
34
+ previous: range(local(year - 1, 0, 1), local(year - 1, 11, 31)),
35
+ };
36
+ }
37
+ case 'custom': {
38
+ const from = custom === undefined ? null : parseLocalDate(custom.from);
39
+ const to = custom === undefined ? null : parseLocalDate(custom.to);
40
+ if (custom === undefined || from === null || to === null || to < from) {
41
+ throw new TypeError('A custom period needs a valid from and to, from not after to.');
42
+ }
43
+ const length = days(from, to);
44
+ return {
45
+ key,
46
+ from: custom.from,
47
+ to: custom.to,
48
+ previous: range(local(from.getFullYear(), from.getMonth(), from.getDate() - length), local(from.getFullYear(), from.getMonth(), from.getDate() - 1)),
49
+ };
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,26 @@
1
+ import type { CubeAppSession } from '../app.js';
2
+ import type { FilterValue } from '../types.js';
3
+ import type { Period } from './period.js';
4
+ export interface PeriodSourceLike extends HTMLElement {
5
+ readonly value: Period;
6
+ updateComplete: Promise<void>;
7
+ }
8
+ /** The `filter` attribute: a JSON object of API filters, or nothing. */
9
+ export declare function blockFilter(el: Element): Record<string, FilterValue>;
10
+ /** `{ [field]: { gte: from, lt: day after to } }`: right for dates and date-times alike, never `between`. */
11
+ export declare function periodFilter(field: string, range: {
12
+ from: string;
13
+ to: string;
14
+ }): Record<string, FilterValue>;
15
+ /**
16
+ * The period picker a block follows, once it has rendered. Null without a selector. Blocks load after the page's
17
+ * scripts have run, so a picker element that is not defined by then never will be: that is an error, not a wait.
18
+ */
19
+ export declare function resolvePeriodSource(host: HTMLElement, selector: string | null): Promise<PeriodSourceLike | null>;
20
+ /** The date field a period filters on: explicit, else the field of a truncated `by`. */
21
+ export declare function dateFieldFor(dateField: string | null, by: string | null, tag: string): string;
22
+ /**
23
+ * Labels for ids from `<resource>.<field>`: one list per 200 ids. An id without a row, or whose list was refused
24
+ * or failed (not an abort), is absent from the map.
25
+ */
26
+ export declare function lookupLabels(session: CubeAppSession, lookup: string, ids: unknown[], signal: AbortSignal): Promise<Map<string, string>>;
@@ -0,0 +1,98 @@
1
+ import { parseDimension, isoDate, parseLocalDate } from './format.js';
2
+ import { NL } from './texts.js';
3
+ const LOOKUP = /^([a-z][a-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/;
4
+ const LOOKUP_CHUNK = 200;
5
+ /** The `filter` attribute: a JSON object of API filters, or nothing. */
6
+ export function blockFilter(el) {
7
+ const raw = el.getAttribute('filter');
8
+ if (raw === null || raw.trim() === '')
9
+ return {};
10
+ let filter;
11
+ try {
12
+ filter = JSON.parse(raw);
13
+ }
14
+ catch {
15
+ filter = null;
16
+ }
17
+ if (filter === null || typeof filter !== 'object' || Array.isArray(filter)) {
18
+ throw new TypeError(NL.invalid.json(el.localName, 'filter'));
19
+ }
20
+ return filter;
21
+ }
22
+ function isPeriod(value) {
23
+ const period = value;
24
+ return typeof period?.from === 'string' && typeof period.to === 'string'
25
+ && typeof period.previous?.from === 'string' && typeof period.previous.to === 'string';
26
+ }
27
+ /** `{ [field]: { gte: from, lt: day after to } }`: right for dates and date-times alike, never `between`. */
28
+ export function periodFilter(field, range) {
29
+ const to = parseLocalDate(range.to);
30
+ if (to === null)
31
+ throw new TypeError(NL.invalid.periodEnd(range.to));
32
+ to.setDate(to.getDate() + 1);
33
+ return { [field]: { gte: range.from, lt: isoDate(to) } };
34
+ }
35
+ /**
36
+ * The period picker a block follows, once it has rendered. Null without a selector. Blocks load after the page's
37
+ * scripts have run, so a picker element that is not defined by then never will be: that is an error, not a wait.
38
+ */
39
+ export async function resolvePeriodSource(host, selector) {
40
+ if (selector === null || selector === '')
41
+ return null;
42
+ const source = host.ownerDocument.querySelector(selector);
43
+ const registry = (host.ownerDocument.defaultView ?? window).customElements;
44
+ if (source === null || registry.get(source.localName) === undefined) {
45
+ throw new TypeError(NL.invalid.periodSource(selector));
46
+ }
47
+ const picker = source;
48
+ await picker.updateComplete;
49
+ if (!isPeriod(picker.value))
50
+ throw new TypeError(NL.invalid.periodSource(selector));
51
+ return picker;
52
+ }
53
+ /** The date field a period filters on: explicit, else the field of a truncated `by`. */
54
+ export function dateFieldFor(dateField, by, tag) {
55
+ if (dateField !== null && dateField !== '')
56
+ return dateField;
57
+ if (by !== null && by !== '') {
58
+ const dimension = parseDimension(by);
59
+ if (dimension.truncation !== null)
60
+ return dimension.field;
61
+ }
62
+ throw new TypeError(NL.invalid.dateField(tag));
63
+ }
64
+ /**
65
+ * Labels for ids from `<resource>.<field>`: one list per 200 ids. An id without a row, or whose list was refused
66
+ * or failed (not an abort), is absent from the map.
67
+ */
68
+ export async function lookupLabels(session, lookup, ids, signal) {
69
+ const match = LOOKUP.exec(lookup);
70
+ const resource = match?.[1];
71
+ const field = match?.[2];
72
+ if (resource === undefined || field === undefined)
73
+ throw new TypeError(NL.invalid.lookup(lookup));
74
+ const unique = [...new Set(ids.filter((id) => id !== null && id !== undefined).map((id) => id))];
75
+ const labels = new Map();
76
+ for (let start = 0; start < unique.length; start += LOOKUP_CHUNK) {
77
+ // The preview session ignores the signal; without this an aborted block would still ask every chunk.
78
+ signal.throwIfAborted();
79
+ const chunk = unique.slice(start, start + LOOKUP_CHUNK);
80
+ let data;
81
+ try {
82
+ const query = { filter: { id: { in: chunk } }, fields: ['id', field], page: { size: LOOKUP_CHUNK } };
83
+ ({ data } = await session.resource(resource).list(query, { signal }));
84
+ }
85
+ catch (error) {
86
+ // Names are decoration: a refused or failing lookup leaves the ids unlabeled instead of failing the block.
87
+ if (signal.aborted || error?.name === 'AbortError')
88
+ throw error;
89
+ continue;
90
+ }
91
+ for (const row of data) {
92
+ const label = row[field];
93
+ if (label !== null && label !== undefined)
94
+ labels.set(String(row.id), String(label));
95
+ }
96
+ }
97
+ return labels;
98
+ }
@@ -0,0 +1,23 @@
1
+ import type { ResourceCapability } from '../types.js';
2
+ export interface FieldInfo {
3
+ name: string;
4
+ type: string;
5
+ format: string | null;
6
+ nullable: boolean;
7
+ readonly: boolean;
8
+ enum: string[] | null;
9
+ description: string | null;
10
+ }
11
+ /** What a block needs to know about one resource, the same shape in the preview and live. */
12
+ export interface ResourceSchema {
13
+ name: string;
14
+ actions: string[];
15
+ fields: FieldInfo[];
16
+ summary: string[];
17
+ filterable: string[];
18
+ sortable: string[];
19
+ groupable: string[];
20
+ aggregatable: Record<string, string[]>;
21
+ }
22
+ /** The REST face has neither `format` nor `enum`, so both are null. */
23
+ export declare function schemaFromCapability(resource: ResourceCapability): ResourceSchema;
@@ -0,0 +1,21 @@
1
+ /** The REST face has neither `format` nor `enum`, so both are null. */
2
+ export function schemaFromCapability(resource) {
3
+ return {
4
+ name: resource.name,
5
+ actions: [...resource.actions],
6
+ fields: resource.fields.map((field) => ({
7
+ name: field.name,
8
+ type: field.type,
9
+ format: null,
10
+ nullable: field.nullable,
11
+ readonly: field.readonly,
12
+ enum: null,
13
+ description: field.description ?? null,
14
+ })),
15
+ summary: [...resource.summary],
16
+ filterable: [...resource.filterable],
17
+ sortable: [...resource.sortable],
18
+ groupable: [...resource.groupable],
19
+ aggregatable: Object.fromEntries(Object.entries(resource.aggregatable).map(([key, fns]) => [key, [...fns]])),
20
+ };
21
+ }
@@ -0,0 +1,8 @@
1
+ import type { BlockScope } from './define.js';
2
+ /**
3
+ * Shared by every block's shadow root. Colours, radii and the font come from the theme's --cube-* properties.
4
+ * Every block's scroller is position:relative, so an .sr-only inside it cannot escape it and widen the page.
5
+ */
6
+ export declare const BASE_CSS = "\n:host{display:block;box-sizing:border-box;min-width:0;font-family:var(--cube-font-family,system-ui,sans-serif);\ncolor:var(--cube-color-text,#1f2328);font-size:1rem;line-height:1.5}\n:host([hidden]){display:none}\n*,*::before,*::after{box-sizing:inherit}\n.cube-body{display:block;min-width:0}\nbutton{font:inherit;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;\ngap:.375rem;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--cube-color-primary);\nborder-radius:var(--cube-radius-small);background:var(--cube-color-primary);color:var(--cube-color-on-primary)}\nbutton:hover{background:var(--cube-color-primary-hover);border-color:var(--cube-color-primary-hover)}\nbutton.secondary{background:var(--cube-color-bg);color:var(--cube-color-primary-text);\nborder-color:var(--cube-color-border)}\nbutton.secondary:hover{background:var(--cube-color-primary-subtle)}\nbutton:disabled{cursor:not-allowed;opacity:.6}\ninput,select,textarea{font:inherit;color:inherit;max-width:100%;min-height:2.5rem;padding:.5rem .75rem;\nborder:1px solid var(--cube-color-text-muted);border-radius:var(--cube-radius-small);background:var(--cube-color-bg)}\na{color:var(--cube-color-primary-text)}\n:focus-visible{outline:3px solid var(--cube-color-focus);outline-offset:2px}\n.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);\nwhite-space:nowrap;border:0}\n.badge{display:inline-block;padding:.125rem .5rem;border-radius:999px;font-size:.875rem;font-weight:600;\nbackground:var(--cube-color-primary-subtle);color:var(--cube-color-primary-text)}\n.muted{color:var(--cube-color-text-muted)}\n.example.badge,.not-saved{background:var(--cube-color-example-bg);color:var(--cube-color-example-text)}\np.example{margin:0 0 .5rem}\ntd.example,dd.example{text-decoration:underline dashed;text-underline-offset:.25em}\n.not-saved{margin:0;align-self:center;padding:.125rem .5rem;border-radius:var(--cube-radius-small);font-size:.875rem;\nfont-weight:600}\n.row{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem}\n@media (max-width:640px){.row{flex-direction:column;align-items:stretch}.row>button{width:100%}}\n";
7
+ /** One constructed stylesheet per scope and css text, shared by every element that adopts it. */
8
+ export declare function sheet(scope: BlockScope, css: string): CSSStyleSheet;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Shared by every block's shadow root. Colours, radii and the font come from the theme's --cube-* properties.
3
+ * Every block's scroller is position:relative, so an .sr-only inside it cannot escape it and widen the page.
4
+ */
5
+ export const BASE_CSS = `
6
+ :host{display:block;box-sizing:border-box;min-width:0;font-family:var(--cube-font-family,system-ui,sans-serif);
7
+ color:var(--cube-color-text,#1f2328);font-size:1rem;line-height:1.5}
8
+ :host([hidden]){display:none}
9
+ *,*::before,*::after{box-sizing:inherit}
10
+ .cube-body{display:block;min-width:0}
11
+ button{font:inherit;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;
12
+ gap:.375rem;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--cube-color-primary);
13
+ border-radius:var(--cube-radius-small);background:var(--cube-color-primary);color:var(--cube-color-on-primary)}
14
+ button:hover{background:var(--cube-color-primary-hover);border-color:var(--cube-color-primary-hover)}
15
+ button.secondary{background:var(--cube-color-bg);color:var(--cube-color-primary-text);
16
+ border-color:var(--cube-color-border)}
17
+ button.secondary:hover{background:var(--cube-color-primary-subtle)}
18
+ button:disabled{cursor:not-allowed;opacity:.6}
19
+ input,select,textarea{font:inherit;color:inherit;max-width:100%;min-height:2.5rem;padding:.5rem .75rem;
20
+ border:1px solid var(--cube-color-text-muted);border-radius:var(--cube-radius-small);background:var(--cube-color-bg)}
21
+ a{color:var(--cube-color-primary-text)}
22
+ :focus-visible{outline:3px solid var(--cube-color-focus);outline-offset:2px}
23
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);
24
+ white-space:nowrap;border:0}
25
+ .badge{display:inline-block;padding:.125rem .5rem;border-radius:999px;font-size:.875rem;font-weight:600;
26
+ background:var(--cube-color-primary-subtle);color:var(--cube-color-primary-text)}
27
+ .muted{color:var(--cube-color-text-muted)}
28
+ .example.badge,.not-saved{background:var(--cube-color-example-bg);color:var(--cube-color-example-text)}
29
+ p.example{margin:0 0 .5rem}
30
+ td.example,dd.example{text-decoration:underline dashed;text-underline-offset:.25em}
31
+ .not-saved{margin:0;align-self:center;padding:.125rem .5rem;border-radius:var(--cube-radius-small);font-size:.875rem;
32
+ font-weight:600}
33
+ .row{display:flex;flex-wrap:wrap;align-items:center;gap:.75rem}
34
+ @media (max-width:640px){.row{flex-direction:column;align-items:stretch}.row>button{width:100%}}
35
+ `;
36
+ const sheets = new WeakMap();
37
+ /** One constructed stylesheet per scope and css text, shared by every element that adopts it. */
38
+ export function sheet(scope, css) {
39
+ let byCss = sheets.get(scope);
40
+ if (byCss === undefined) {
41
+ byCss = new Map();
42
+ sheets.set(scope, byCss);
43
+ }
44
+ let found = byCss.get(css);
45
+ if (found === undefined) {
46
+ found = new scope.CSSStyleSheet();
47
+ found.replaceSync(css);
48
+ byCss.set(css, found);
49
+ }
50
+ return found;
51
+ }
@@ -0,0 +1,134 @@
1
+ /** On its own so the core (the preview session) takes this one name, not every UI text. */
2
+ export declare const PREVIEW_USER_NAME = "Voorbeeldgebruiker";
3
+ /** The theme's warnings, also on their own: resolveTheme is part of the core. */
4
+ export declare const THEME_TEXTS: {
5
+ readonly invalidPrimary: "De hoofdkleur is geen geldige hexkleur; de neutrale standaardkleur wordt gebruikt.";
6
+ readonly invalidAccent: "De accentkleur is geen geldige hexkleur; er wordt een accentkleur afgeleid.";
7
+ readonly primaryLight: "De hoofdkleur is erg licht; witte tekst erop heeft weinig contrast.";
8
+ readonly primaryDarkened: "De hoofdkleur is voor tekst donkerder gemaakt om voldoende contrast te houden.";
9
+ };
10
+ /** Every Dutch text the UI shows. Blocks take their wording from here, never from a literal of their own. */
11
+ export declare const NL: {
12
+ readonly loading: "Laden…";
13
+ readonly empty: "Er is hier nog niets te tonen.";
14
+ readonly error: "Er ging iets mis bij het laden.";
15
+ readonly retry: "Opnieuw proberen";
16
+ readonly forbidden: "Je hebt geen toegang tot deze gegevens.";
17
+ readonly signedOut: "Je bent niet ingelogd.";
18
+ readonly signIn: "Inloggen";
19
+ readonly signOut: "Uitloggen";
20
+ readonly signedIn: "Ingelogd";
21
+ readonly previewUser: "Voorbeeldgebruiker";
22
+ readonly pages: "Pagina's";
23
+ readonly yes: "Ja";
24
+ readonly no: "Nee";
25
+ readonly emptyValue: "—";
26
+ readonly quarter: (quarter: string, year: string) => string;
27
+ readonly week: (week: number, year: string) => string;
28
+ /** A short month on an axis with its year: `jan '26`. */
29
+ readonly monthYear: (month: string, year: string) => string;
30
+ readonly period: {
31
+ readonly label: "Periode";
32
+ readonly 'this-month': "Deze maand";
33
+ readonly '12m': "Laatste 12 maanden";
34
+ readonly 'this-year': "Dit jaar";
35
+ readonly custom: "Zelf kiezen";
36
+ readonly from: "Van";
37
+ readonly to: "Tot en met";
38
+ readonly invalidRange: "De einddatum ligt vóór de begindatum.";
39
+ readonly incompleteDate: "Vul bij Van en Tot en met een volledige datum in.";
40
+ };
41
+ readonly noData: "geen gegevens";
42
+ readonly comparedTo: "t.o.v. vorige periode";
43
+ readonly newValue: "nieuw";
44
+ readonly higher: "hoger";
45
+ readonly lower: "lager";
46
+ readonly incomplete: "Mogelijk niet compleet: er zijn meer groepen dan de DMP in één keer teruggeeft.";
47
+ readonly asTable: "Gegevens van de grafiek";
48
+ readonly tooManyGroups: "Te veel groepen om te tonen (meer dan 1000). Kies een kortere periode of een filter.";
49
+ readonly points: (count: number) => string;
50
+ readonly change: (percent: string, direction: string) => string;
51
+ readonly search: "Zoeken";
52
+ readonly all: "Alle";
53
+ readonly min: "Vanaf";
54
+ readonly max: "Tot en met";
55
+ readonly previous: "Vorige";
56
+ readonly next: "Volgende";
57
+ readonly pageOf: (n: number, m: number, total: number) => string;
58
+ readonly exportCsv: "Exporteren (CSV)";
59
+ readonly exporting: (n: number) => string;
60
+ readonly chooseRow: "Kies een rij om de details te zien.";
61
+ readonly edit: "Bewerken";
62
+ readonly save: "Opslaan";
63
+ readonly cancel: "Annuleren";
64
+ readonly saved: "Opgeslagen.";
65
+ readonly formSaved: "Opgeslagen. Dank je wel!";
66
+ readonly another: "Nog een invullen";
67
+ readonly required: "Vul dit veld in.";
68
+ /** After the label of a field that must be filled in. */
69
+ readonly requiredMark: " (verplicht)";
70
+ readonly notANumber: "Vul een getal in.";
71
+ readonly notAnInteger: "Vul een geheel getal in.";
72
+ readonly conflict: "Iemand anders heeft deze rij intussen gewijzigd. Laad de nieuwste versie en probeer het opnieuw.";
73
+ readonly reload: "Opnieuw laden";
74
+ readonly saveFailed: "Opslaan is niet gelukt. Probeer het opnieuw.";
75
+ readonly exportFailed: "Exporteren is niet gelukt. Probeer het opnieuw.";
76
+ /** The form-level line; `invalid` itself already holds the attribute errors. */
77
+ readonly formInvalid: "Niet alles is goed ingevuld.";
78
+ readonly noWritableFields: "Dit formulier heeft geen invulbare velden.";
79
+ readonly calcFailed: "De berekening lukt niet met deze invoer.";
80
+ readonly moveTo: (label: string) => string;
81
+ readonly moveFailed: "Verplaatsen lukte niet.";
82
+ readonly tryAgain: "Probeer het opnieuw.";
83
+ readonly changedMeanwhile: "Iemand anders heeft dit intussen gewijzigd; de nieuwste stand is geladen.";
84
+ readonly more: (n: number) => string;
85
+ readonly noColumns: "Geef de kolommen op met <cube-column value=\"…\">.";
86
+ readonly emptyColumn: "Leeg";
87
+ readonly range: (min: string, max: string) => string;
88
+ readonly atLeast: (min: string) => string;
89
+ readonly atMost: (max: string) => string;
90
+ readonly step: (step: string) => string;
91
+ /** The visible word of a marked column; the rest of exampleMissing is for screen readers. */
92
+ readonly example: "Voorbeeld";
93
+ readonly exampleMissing: "Voorbeeld — bestaat nog niet";
94
+ readonly exampleNotSaved: "Voorbeeld — wordt niet opgeslagen";
95
+ readonly gaps: {
96
+ readonly screen: "Scherm";
97
+ readonly what: "Wat ontbreekt";
98
+ readonly type: "Soort gegeven";
99
+ readonly why: "Waarom";
100
+ readonly none: "Er ontbreekt niets: alles in dit voorstel bestaat al in de DMP.";
101
+ readonly types: {
102
+ readonly tekst: "Tekst";
103
+ readonly getal: "Getal";
104
+ readonly bedrag: "Bedrag";
105
+ readonly datum: "Datum";
106
+ readonly keuze: "Keuze";
107
+ readonly 'ja/nee': "Ja/nee";
108
+ };
109
+ };
110
+ readonly invalid: {
111
+ readonly json: (tag: string, name: string) => string;
112
+ readonly number: (tag: string, name: string) => string;
113
+ readonly attribute: (name: string) => string;
114
+ readonly measure: (measure: string) => string;
115
+ readonly missing: (tag: string, name: string) => string;
116
+ readonly value: (tag: string, name: string, value: string) => string;
117
+ readonly formula: "cube-calc: formula moet een functie zijn";
118
+ readonly calcSingle: "cube-calc: formula gaf één waarde, maar er zijn meer cube-results; geef een object met hun namen";
119
+ readonly dateField: (tag: string) => string;
120
+ readonly compare: (tag: string) => string;
121
+ readonly periodSource: (selector: string) => string;
122
+ readonly periodEnd: (to: string) => string;
123
+ readonly notFilterable: (tag: string, name: string, field: string) => string;
124
+ readonly selector: (tag: string, name: string, selector: string) => string;
125
+ readonly lookup: (lookup: string) => string;
126
+ readonly dimension: (by: string) => string;
127
+ };
128
+ readonly theme: {
129
+ readonly invalidPrimary: "De hoofdkleur is geen geldige hexkleur; de neutrale standaardkleur wordt gebruikt.";
130
+ readonly invalidAccent: "De accentkleur is geen geldige hexkleur; er wordt een accentkleur afgeleid.";
131
+ readonly primaryLight: "De hoofdkleur is erg licht; witte tekst erop heeft weinig contrast.";
132
+ readonly primaryDarkened: "De hoofdkleur is voor tekst donkerder gemaakt om voldoende contrast te houden.";
133
+ };
134
+ };
@@ -0,0 +1,122 @@
1
+ /** On its own so the core (the preview session) takes this one name, not every UI text. */
2
+ export const PREVIEW_USER_NAME = 'Voorbeeldgebruiker';
3
+ /** The theme's warnings, also on their own: resolveTheme is part of the core. */
4
+ export const THEME_TEXTS = {
5
+ invalidPrimary: 'De hoofdkleur is geen geldige hexkleur; de neutrale standaardkleur wordt gebruikt.',
6
+ invalidAccent: 'De accentkleur is geen geldige hexkleur; er wordt een accentkleur afgeleid.',
7
+ primaryLight: 'De hoofdkleur is erg licht; witte tekst erop heeft weinig contrast.',
8
+ primaryDarkened: 'De hoofdkleur is voor tekst donkerder gemaakt om voldoende contrast te houden.',
9
+ };
10
+ /** Every Dutch text the UI shows. Blocks take their wording from here, never from a literal of their own. */
11
+ export const NL = {
12
+ loading: 'Laden…',
13
+ empty: 'Er is hier nog niets te tonen.',
14
+ error: 'Er ging iets mis bij het laden.',
15
+ retry: 'Opnieuw proberen',
16
+ forbidden: 'Je hebt geen toegang tot deze gegevens.',
17
+ signedOut: 'Je bent niet ingelogd.',
18
+ signIn: 'Inloggen',
19
+ signOut: 'Uitloggen',
20
+ signedIn: 'Ingelogd',
21
+ previewUser: PREVIEW_USER_NAME,
22
+ pages: "Pagina's",
23
+ yes: 'Ja',
24
+ no: 'Nee',
25
+ emptyValue: '—',
26
+ quarter: (quarter, year) => `K${quarter} ${year}`,
27
+ week: (week, year) => `week ${week}, ${year}`,
28
+ /** A short month on an axis with its year: `jan '26`. */
29
+ monthYear: (month, year) => `${month} '${year.slice(-2)}`,
30
+ period: {
31
+ label: 'Periode',
32
+ 'this-month': 'Deze maand',
33
+ '12m': 'Laatste 12 maanden',
34
+ 'this-year': 'Dit jaar',
35
+ custom: 'Zelf kiezen',
36
+ from: 'Van',
37
+ to: 'Tot en met',
38
+ invalidRange: 'De einddatum ligt vóór de begindatum.',
39
+ incompleteDate: 'Vul bij Van en Tot en met een volledige datum in.',
40
+ },
41
+ noData: 'geen gegevens',
42
+ comparedTo: 't.o.v. vorige periode',
43
+ newValue: 'nieuw',
44
+ higher: 'hoger',
45
+ lower: 'lager',
46
+ incomplete: 'Mogelijk niet compleet: er zijn meer groepen dan de DMP in één keer teruggeeft.',
47
+ asTable: 'Gegevens van de grafiek',
48
+ tooManyGroups: 'Te veel groepen om te tonen (meer dan 1000). Kies een kortere periode of een filter.',
49
+ points: (count) => `${count} ${count === 1 ? 'punt' : 'punten'}`,
50
+ change: (percent, direction) => `${percent} procent ${direction} dan de vorige periode`,
51
+ search: 'Zoeken',
52
+ all: 'Alle',
53
+ min: 'Vanaf',
54
+ max: 'Tot en met',
55
+ previous: 'Vorige',
56
+ next: 'Volgende',
57
+ pageOf: (n, m, total) => `Pagina ${n} van ${m} · ${total} rijen`,
58
+ exportCsv: 'Exporteren (CSV)',
59
+ exporting: (n) => `${n} rijen opgehaald…`,
60
+ chooseRow: 'Kies een rij om de details te zien.',
61
+ edit: 'Bewerken',
62
+ save: 'Opslaan',
63
+ cancel: 'Annuleren',
64
+ saved: 'Opgeslagen.',
65
+ formSaved: 'Opgeslagen. Dank je wel!',
66
+ another: 'Nog een invullen',
67
+ required: 'Vul dit veld in.',
68
+ /** After the label of a field that must be filled in. */
69
+ requiredMark: ' (verplicht)',
70
+ notANumber: 'Vul een getal in.',
71
+ notAnInteger: 'Vul een geheel getal in.',
72
+ conflict: 'Iemand anders heeft deze rij intussen gewijzigd. Laad de nieuwste versie en probeer het opnieuw.',
73
+ reload: 'Opnieuw laden',
74
+ saveFailed: 'Opslaan is niet gelukt. Probeer het opnieuw.',
75
+ exportFailed: 'Exporteren is niet gelukt. Probeer het opnieuw.',
76
+ /** The form-level line; `invalid` itself already holds the attribute errors. */
77
+ formInvalid: 'Niet alles is goed ingevuld.',
78
+ noWritableFields: 'Dit formulier heeft geen invulbare velden.',
79
+ calcFailed: 'De berekening lukt niet met deze invoer.',
80
+ moveTo: (label) => `Verplaats naar ${label}`,
81
+ moveFailed: 'Verplaatsen lukte niet.',
82
+ tryAgain: 'Probeer het opnieuw.',
83
+ changedMeanwhile: 'Iemand anders heeft dit intussen gewijzigd; de nieuwste stand is geladen.',
84
+ more: (n) => `en nog ${n}`,
85
+ noColumns: 'Geef de kolommen op met <cube-column value="…">.',
86
+ emptyColumn: 'Leeg',
87
+ range: (min, max) => `Kies een waarde van ${min} tot en met ${max}.`,
88
+ atLeast: (min) => `Kies een waarde van ${min} of meer.`,
89
+ atMost: (max) => `Kies een waarde van ten hoogste ${max}.`,
90
+ step: (step) => `Kies een waarde in stappen van ${step}.`,
91
+ /** The visible word of a marked column; the rest of exampleMissing is for screen readers. */
92
+ example: 'Voorbeeld',
93
+ exampleMissing: 'Voorbeeld — bestaat nog niet',
94
+ exampleNotSaved: 'Voorbeeld — wordt niet opgeslagen',
95
+ gaps: {
96
+ screen: 'Scherm',
97
+ what: 'Wat ontbreekt',
98
+ type: 'Soort gegeven',
99
+ why: 'Waarom',
100
+ none: 'Er ontbreekt niets: alles in dit voorstel bestaat al in de DMP.',
101
+ types: { tekst: 'Tekst', getal: 'Getal', bedrag: 'Bedrag', datum: 'Datum', keuze: 'Keuze', 'ja/nee': 'Ja/nee' },
102
+ },
103
+ invalid: {
104
+ json: (tag, name) => `${tag}: ${name} is geen geldige JSON`,
105
+ number: (tag, name) => `${tag}: ${name} is geen geldig getal`,
106
+ attribute: (name) => `${name} mag geen attribuut zijn: geen inline stijl of script`,
107
+ measure: (measure) => `"${measure}" is geen geldige meting: gebruik count of count/sum/avg/min/max(veld)`,
108
+ missing: (tag, name) => `${tag}: ${name} ontbreekt`,
109
+ value: (tag, name, value) => `${tag}: ${name} "${value}" is geen geldige waarde`,
110
+ formula: 'cube-calc: formula moet een functie zijn',
111
+ calcSingle: 'cube-calc: formula gaf één waarde, maar er zijn meer cube-results; geef een object met hun namen',
112
+ dateField: (tag) => `${tag}: date-field ontbreekt bij period`,
113
+ compare: (tag) => `${tag}: compare werkt alleen samen met period`,
114
+ periodSource: (selector) => `Geen periodekiezer gevonden voor "${selector}"`,
115
+ periodEnd: (to) => `"${to}" is geen geldige einddatum van de periode`,
116
+ notFilterable: (tag, name, field) => `${tag}: ${name} noemt "${field}", maar daarop kan niet gefilterd worden`,
117
+ selector: (tag, name, selector) => `${tag}: ${name} "${selector}" is geen geldige selector`,
118
+ lookup: (lookup) => `"${lookup}" is geen geldige opzoeking: gebruik resource.veld`,
119
+ dimension: (by) => `"${by}" is geen geldige indeling: gebruik veld of veld.day/week/month/quarter/year`,
120
+ },
121
+ theme: THEME_TEXTS,
122
+ };
@@ -0,0 +1,58 @@
1
+ export type FontId = 'system' | 'inter' | 'albert-sans' | 'source-sans-3' | 'nunito-sans' | 'ibm-plex-sans';
2
+ export type Corners = 'scherp' | 'normaal' | 'rond';
3
+ export interface ThemeSource {
4
+ primary_colour: string;
5
+ accent_colour?: string | null;
6
+ font?: FontId;
7
+ corners?: Corners;
8
+ logo?: string | null;
9
+ logo_dark?: string | null;
10
+ }
11
+ export interface FontFile {
12
+ src: string;
13
+ weight?: string;
14
+ style?: 'normal' | 'italic';
15
+ }
16
+ export declare const TOKEN_NAMES: readonly ["color-primary", "color-primary-hover", "color-primary-subtle", "color-primary-text", "color-on-primary", "color-accent", "color-on-accent", "color-text", "color-text-muted", "color-bg", "color-surface", "color-border", "color-focus", "color-danger", "color-success", "color-warning", "color-example-bg", "color-example-text", "color-series-1", "color-series-2", "color-series-3", "color-series-4", "color-series-5", "color-series-6", "radius", "radius-small", "font-family"];
17
+ export type TokenName = (typeof TOKEN_NAMES)[number];
18
+ export type ThemeTokens = Record<TokenName, string>;
19
+ export interface ThemeFile {
20
+ theme: 1;
21
+ captured_at?: string;
22
+ source: ThemeSource;
23
+ tokens?: Partial<Record<TokenName, string>>;
24
+ font_files?: FontFile[];
25
+ }
26
+ export interface ThemeWarning {
27
+ code: 'invalid-primary' | 'invalid-accent' | 'primary-light' | 'primary-darkened-for-text';
28
+ message: string;
29
+ }
30
+ export declare const NEUTRAL_SOURCE: ThemeSource;
31
+ type FontEntry = {
32
+ family: string;
33
+ files: ReadonlyArray<{
34
+ file: string;
35
+ weight: string;
36
+ style: 'normal';
37
+ }>;
38
+ };
39
+ export declare const FONTS: Readonly<Record<Exclude<FontId, 'system'>, FontEntry>>;
40
+ export declare function contrastRatio(a: string, b: string): number;
41
+ export declare function deriveTheme(source: ThemeSource): {
42
+ tokens: ThemeTokens;
43
+ warnings: ThemeWarning[];
44
+ };
45
+ export interface ResolvedTheme {
46
+ tokens: ThemeTokens;
47
+ logo: string | null;
48
+ logoDark: string | null;
49
+ fontFamily: string | null;
50
+ fontFiles: FontFile[];
51
+ warnings: ThemeWarning[];
52
+ }
53
+ export declare function resolveTheme(input: ThemeFile | ThemeSource | null, baseUrl: string): ResolvedTheme;
54
+ export declare function themeCss(theme: ResolvedTheme): string;
55
+ export declare function currentTheme(): ResolvedTheme;
56
+ export declare function setTheme(theme: ResolvedTheme): void;
57
+ export declare function onTheme(listener: (theme: ResolvedTheme) => void): () => void;
58
+ export {};