@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
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @cube/dmp-sdk
1
+ # @cube-nl/dmp-sdk
2
2
 
3
3
  **Nederlands.** De TypeScript-SDK voor het DMP: één client waarmee een app de REST-API van
4
4
  `cube/api` aanroept, in de browser of in Node 20+. Zonder runtime-dependencies: alleen `fetch`,
@@ -9,8 +9,9 @@
9
9
  een persoonlijk token voor een PoC. Het token blijft standaard alleen in het geheugen.
10
10
 
11
11
  Dit pakket vervangt `dmp.js` en `oauth.js` uit de hackathon-starter van `cube/resources`
12
- (`docs/hackathon/starter/public/`): dezelfde logica, getypt en getest. Het staat nog in **geen
13
- enkele registry**; installeer het voorlopig uit git of uit het `npm pack`-artefact van de CI.
12
+ (`docs/hackathon/starter/public/`): dezelfde logica, getypt en getest. Het staat openbaar op npm:
13
+ `npm install @cube-nl/dmp-sdk@next` (zolang het een release candidate is), of als losse
14
+ `cube-sdk.js` via jsDelivr of unpkg.
14
15
 
15
16
  Besluit OP-05: **één client, resources als eigenschap** —
16
17
  `createClient(...).resources('projects').list()`, geen ingang per soort.
@@ -28,22 +29,35 @@ The rest of this document is in English.
28
29
  - [Errors](#errors)
29
30
  - [Voor AI-tools](#voor-ai-tools)
30
31
  - [Route B: apps without a build step](#route-b-apps-without-a-build-step)
32
+ - [Bouwstenen (UI)](#bouwstenen-ui)
31
33
  - [What this SDK deliberately does not do](#what-this-sdk-deliberately-does-not-do)
32
34
  - [Development](#development)
33
35
  - [Integration tests](#integration-tests)
34
36
 
35
37
  ## Install
36
38
 
37
- Not published yet. Until it is:
39
+ Published on the public npm registry as [`@cube-nl/dmp-sdk`](https://www.npmjs.com/package/@cube-nl/dmp-sdk):
38
40
 
39
41
  ```bash
40
- npm install git+ssh://git@gitlab.cube.nl/cube/packages/laravel/filament/dmp/sdk-js.git#5.0.x
41
- # or the tarball the CI's pack job keeps for a week:
42
- npm install ./cube-dmp-sdk-1.0.0-rc.3.tgz
42
+ npm install @cube-nl/dmp-sdk@next # while 1.0.0 is a release candidate
43
+ npm install @cube-nl/dmp-sdk # once a stable version is out
43
44
  ```
44
45
 
45
- A git install runs `prepare`, which builds `dist/` with `tsc`. The package is an ES module with
46
- its own `.d.ts`; there is no CommonJS build.
46
+ Release candidates go to the `next` dist-tag, stable versions to `latest`. The package is an ES
47
+ module with its own `.d.ts`; there is no CommonJS build.
48
+
49
+ For an app without a build step, the single-file browser build is on the npm CDNs. Pin an exact
50
+ version:
51
+
52
+ ```html
53
+ <script src="https://cdn.jsdelivr.net/npm/@cube-nl/dmp-sdk@1.1.0-rc.1/dist/cube-sdk.js"></script>
54
+ <!-- or -->
55
+ <script src="https://unpkg.com/@cube-nl/dmp-sdk@1.1.0-rc.1/dist/cube-sdk.js"></script>
56
+ ```
57
+
58
+ Fallback, for an unreleased commit: `npm install
59
+ git+ssh://git@gitlab.cube.nl/cube/packages/laravel/filament/dmp/sdk-js.git#5.0.x` (needs access to
60
+ our GitLab; `prepare` builds `dist/`).
47
61
 
48
62
  ## Quick start
49
63
 
@@ -51,7 +65,7 @@ These ten lines ran as they are (24-09-2026, Node 24, installed from `npm pack`)
51
65
  hackathon demo DMP on `127.0.0.1:8000`, with a personal token in `DMP_TOKEN`:
52
66
 
53
67
  ```js
54
- import { createClient } from '@cube/dmp-sdk';
68
+ import { createClient } from '@cube-nl/dmp-sdk';
55
69
 
56
70
  const dmp = createClient({ baseUrl: 'http://127.0.0.1:8000/apps/api/v1', auth: process.env.DMP_TOKEN });
57
71
 
@@ -97,7 +111,7 @@ Every request carries `Authorization: Bearer <token>`. The API refuses a browser
97
111
  ### OAuth 2.1 + PKCE (the right way for an app)
98
112
 
99
113
  ```ts
100
- import { createClient, createOAuth } from '@cube/dmp-sdk';
114
+ import { createClient, createOAuth } from '@cube-nl/dmp-sdk';
101
115
 
102
116
  const oauth = createOAuth({
103
117
  issuer: 'https://dmp.example.nl', // discovery: {issuer}/.well-known/oauth-authorization-server
@@ -181,7 +195,7 @@ Token limits the API applies to an OAuth token: it reaches **no custom operation
181
195
  ### A personal token or an API-client token
182
196
 
183
197
  ```ts
184
- import { bearer, createClient } from '@cube/dmp-sdk';
198
+ import { bearer, createClient } from '@cube-nl/dmp-sdk';
185
199
 
186
200
  createClient({ baseUrl, auth: process.env.DMP_TOKEN }); // a string
187
201
  createClient({ baseUrl, auth: () => tokenFromSomewhere() }); // read on every call
@@ -282,7 +296,7 @@ not from assumptions.
282
296
  ## Writing
283
297
 
284
298
  ```ts
285
- import { newIdempotencyKey } from '@cube/dmp-sdk';
299
+ import { newIdempotencyKey } from '@cube-nl/dmp-sdk';
286
300
 
287
301
  const projects = dmp.resources('projects');
288
302
 
@@ -344,7 +358,7 @@ name, and an OAuth token reaches none. Names starting with `_` are reserved and
344
358
  Every non-2xx answer throws a `DmpError`:
345
359
 
346
360
  ```ts
347
- import { isDmpError } from '@cube/dmp-sdk';
361
+ import { isDmpError } from '@cube-nl/dmp-sdk';
348
362
 
349
363
  try {
350
364
  await dmp.resources('projects').list({ sort: 'nope' });
@@ -548,7 +562,14 @@ the REST face reads it:
548
562
  Laravel's sentence per field; an empty body `422 validation_failed`. `ifMatch` with a stale
549
563
  `recordVersion` is `409 conflict`; `idempotencyKey` replays the first answer.
550
564
  - `aggregate` with `count`, `count(f)`, `sum(f)`, `avg(f)`, `min(f)`, `max(f)`, grouped on fields
551
- and on `.year`, `.month` and `.day`.
565
+ and on `.day`, `.week` (ISO, `2026-W23`), `.month`, `.quarter` (`2026-Q2`) and `.year`; a date-time is
566
+ bucketed by the date as written. A call needs `aggregate` in the manifest's `uses` (`list` alone no longer
567
+ suffices). Grouping is limited to the schema's `groupable` fields and a function to what `aggregatable` allows
568
+ (absent: every field, every function; a bare `count` is always allowed). Groups come ascending by their values,
569
+ nulls first; a `sum` of nothing is `null`; a whole-set aggregate over no rows answers one row. At most 5
570
+ dimensions and 1000 groups; a repeated entry is refused.
571
+ - A field with `choices` in the schema: a value outside `values` (else the field's own `enum`) is
572
+ `422 validation_failed` ("The selected status is invalid."), and a choice that is not `writable` is read-only.
552
573
  - With a `cube-app.json` next to the page (or the `manifest` option), a call outside its `uses` is
553
574
  `403 client_scope_exceeded`, as the live client is capped to it.
554
575
  - Signed out (`signOut()`), every call is `401 token_invalid`.
@@ -586,13 +607,17 @@ synthetic rows.
586
607
  Ship it as `cube-mock.json` next to `index.html`, or register it in a script before the app's
587
608
  module runs: `CubeApp.mock({ ... })`. A resource without a `schema` takes its fields from its rows.
588
609
 
610
+ The schema may also carry three keys that `cube.mock_data` writes: `groupable` (fields an aggregate may group on),
611
+ `aggregatable` (per field, the functions allowed) and `choices` (per field, `{ "values": [...], "writable": true }`).
612
+ Without them the preview is permissive: every field, every function; only a field's own `enum` is checked on writes.
613
+
589
614
  ### `CubeApp` API
590
615
 
591
616
  | | |
592
617
  |---|---|
593
618
  | `CubeApp.connect(options?)` | `Promise<CubeAppSession>`. Options: `mock`, `manifest` (an object, or `false` to check nothing), `pageUrl` (default the page's URL), `fetch`, `tokenStorage` (`'session'`, the default, or `'memory'`), `autoSignIn` (default `false`) |
594
619
  | `CubeApp.mock(data)` | Registers preview data for the next `connect()`; `null` forgets it |
595
- | `CubeApp.version` | This SDK's version, `'0.2.0'` |
620
+ | `CubeApp.version` | This SDK's version, e.g. `'1.1.0-rc.1'` |
596
621
  | `CubeApp.sdk` | The manifest `"sdk"` this build serves, `'1'` |
597
622
  | `session.mode` | `'preview'` or `'live'` |
598
623
  | `session.user()` | `{ id, name, scopes }` or `null` |
@@ -600,6 +625,11 @@ module runs: `CubeApp.mock({ ... })`. A resource without a `schema` takes its fi
600
625
  | `session.signOut()` | Forgets the tokens in this browser (not a revocation). Preview: signs the stand-in out |
601
626
  | `session.resource(name)` | A `ResourceClient`: `list`, `pages`, `all`, `get`, `aggregate`, `create`, `update`, `delete`, `operation`, `collectionOperation` |
602
627
  | `session.signInError` | The error the last sign-in came back with (a refused consent: `code` `access_denied`), else `null`; `connect()` does not reject for it |
628
+ | `session.schema(name)` | `Promise<ResourceSchema \| null>`: one resource as a block sees it (fields, actions, what may be filtered, sorted, grouped); `null` when unknown. In a wireframe it includes the missing fields |
629
+ | `session.soort` | What the app is: live the config's `soort`, in the preview the manifest's (`'wireframe'`, say); `null` when neither says |
630
+ | `session.gaps` | A wireframe's `ontbreekt` entries (see [Wireframe](#wireframe)); `[]` for any other app |
631
+ | `session.isExample(resource, field?)` | Whether a wireframe shows invented data there: the whole resource, or that field of it, does not exist yet |
632
+ | `session.simulatesWrites` | `true` in a wireframe: writes are answered in the browser and never saved |
603
633
 
604
634
  Live, `connect()` handles the OAuth callback itself (on the `redirectUri`), so the redirect URI can
605
635
  be the app's own `index.html`. The tokens go to `sessionStorage` by default, unlike `createOAuth`'s
@@ -607,21 +637,306 @@ memory default: a no-build app reloads often, and a redirect round trip on every
607
637
  its users than a token that lives as long as the tab. `tokenStorage: 'memory'` opts out. `user()` reads
608
638
  the id and scopes from the access token for display; the DMP checks the token on every request.
609
639
 
610
- `CubeApp` is also exported from the ES module (`import { CubeApp } from '@cube/dmp-sdk'`) for an
640
+ `CubeApp` is also exported from the ES module (`import { CubeApp } from '@cube-nl/dmp-sdk'`) for an
611
641
  app with a build (route A).
612
642
 
613
643
  ### For the host: serving `cube-sdk.js`
614
644
 
615
645
  | | |
616
646
  |---|---|
617
- | File | `dist/cube-sdk.js` in the package, `@cube/dmp-sdk/cube-sdk.js` as an export; the CI's `pack` job also keeps it as an artifact |
618
- | Size | 28.7 KiB minified, 10.3 KiB gzipped (0.2.0) |
619
- | Version | The banner's first line, `/*! @cube/dmp-sdk 0.2.0 cube-sdk.js … */`, and `CubeApp.version` |
647
+ | File | `dist/cube-sdk.js` in the package, `@cube-nl/dmp-sdk/cube-sdk.js` as an export; on jsDelivr/unpkg as `@cube-nl/dmp-sdk@<version>/dist/cube-sdk.js`; the CI's `pack` job also keeps it as an artifact |
648
+ | Size | 126.9 KiB minified, 42.4 KiB gzipped (17.0 KiB core + 25.5 KiB blocks) in 1.1.0-rc.1; `dist/size.json` has the exact numbers |
649
+ | Version | The banner's first line, `/*! @cube-nl/dmp-sdk 1.1.0-rc.1 cube-sdk.js … */`, and `CubeApp.version` |
650
+
651
+ Pin it by copying the built file of a known version into the host, rather than fetching it at
652
+ runtime: `npm pack @cube-nl/dmp-sdk@<version>` downloads the tarball, `package/dist/cube-sdk.js` is
653
+ the file. Nothing from a CDN, nothing at request time.
654
+
655
+ ## Bouwstenen (UI)
656
+
657
+ **Nederlands.** `cube-sdk.js` bevat ook kant-en-klare bouwstenen: custom elements met het voorvoegsel `cube-`
658
+ die zelf hun gegevens ophalen, de huisstijl van de klant volgen en onder de CSP van een gehost DMP-app
659
+ werken. De eerste set is `cube-app-shell` (kader met kop, navigatie en inloggen), `cube-page` (één pagina van
660
+ het kader) en `cube-state` (laden, leeg, fout, geen toegang, niet ingelogd). Alle teksten zijn Nederlands en
661
+ getallen, bedragen en datums zijn `nl-NL`.
662
+
663
+ ```html
664
+ <script src="./cube-sdk.js"></script>
665
+ <script type="module" src="./app.js"></script>
666
+
667
+ <cube-app-shell name="Verkoop">
668
+ <cube-page name="overzicht" label="Overzicht">
669
+ <cube-state kind="empty" message="Nog geen bestellingen deze week."></cube-state>
670
+ </cube-page>
671
+ <cube-page name="details" label="Details">...</cube-page>
672
+ </cube-app-shell>
673
+ ```
674
+
675
+ How the blocks find the session: a block never signs in by itself. It takes the session the app made with
676
+ `CubeApp.connect()` (`CubeApp.ready`), so a block that sits in the page before the script runs still uses the app's
677
+ session. **Call `CubeApp.connect()` before any `await`** in your script: start it first and await it after.
678
+ Set the house style in the same call, `CubeApp.connect({ theme })`, or call `CubeApp.theme()` before `connect()`: a
679
+ `CubeApp.theme()` call made after `connect()` can be painted over by the theme the start-up applies.
680
+
681
+ ```js
682
+ const session = CubeApp.connect(); // first, so the blocks take this session
683
+ const extra = await (await fetch('./extra.json')).json();
684
+ await session;
685
+ ```
686
+
687
+ An app that never connects gets a session with the defaults, but only once the page has fully loaded (the window's
688
+ `load` event, plus one task), so an app that connects a little later still gets there first and the page signs in
689
+ once. If the app connects after that default session exists, its own session replaces it: `CubeApp.connect()`
690
+ dispatches `cube-session-change` on the document and every block takes the new one. That is a safety net, not a
691
+ way to start: in a live app the default start-up has then handled the sign-in callback with the default options.
692
+ A 401 shows "niet ingelogd", a 403 "geen toegang" (with the right ARIA role), never the raw message. After signing
693
+ in or out the shell dispatches `cube-session-change` on the document and every block loads again.
694
+
695
+ With the classic script the blocks are defined when `cube-sdk.js` loads, and `window.CubeApp` also has
696
+ `CubeApp.defineBlocks(options?)`. On a page without `customElements` (an old engine, a server) it does nothing and
697
+ `CubeApp` still works. Through npm the blocks are not defined on import: call `defineBlocks` yourself.
698
+
699
+ ```js
700
+ import { CubeApp, defineBlocks } from '@cube-nl/dmp-sdk';
701
+
702
+ defineBlocks(); // the ESM `CubeApp` has no defineBlocks; import it from the package
703
+ await CubeApp.connect();
704
+ ```
705
+
706
+ `defineBlocks({ session })` takes a function returning the session the blocks use; the default is `CubeApp.ready`.
707
+
708
+ The manifest `dist/cube-ui.manifest.json` (export `@cube-nl/dmp-sdk/cube-ui.manifest.json`) lists every element
709
+ with its attributes, events, an example and the version it appeared in (`since`), plus the 27 theme tokens and
710
+ the fonts. Its shape is final for 1.1. It is what Claude reads to compose an app. The build also writes `dist/size.json`
711
+ (`core_gzip`, `total_gzip`, `ui_gzip`, `budget_gzip`): the blocks may add at most 40 KiB gzipped to the core and the
712
+ build fails above it.
713
+
714
+ ### Huisstijl
715
+
716
+ The house style is one file next to `index.html`: `cube-theme.json`. Its shape below is final for 1.1. The DMP writes it when an app is published;
717
+ without it (or with a broken one) the neutral theme applies and the app starts anyway. Read once by
718
+ `CubeApp.connect()`, applied to every block through `adoptedStyleSheets`.
719
+
720
+ ```json
721
+ {
722
+ "theme": 1,
723
+ "captured_at": "2026-10-19T09:00:00Z",
724
+ "source": {
725
+ "primary_colour": "#0b5fff",
726
+ "accent_colour": null,
727
+ "font": "inter",
728
+ "corners": "normaal",
729
+ "logo": "cube-theme/logo.svg",
730
+ "logo_dark": null
731
+ },
732
+ "tokens": { "color-danger": "#b42318" },
733
+ "font_files": [{ "src": "cube-theme/inter.woff2", "weight": "100 900", "style": "normal" }]
734
+ }
735
+ ```
736
+
737
+ | Key | Meaning |
738
+ |---|---|
739
+ | `source.primary_colour` | `#rrggbb`; everything else is derived from it. An invalid value falls back to the neutral `#475467` |
740
+ | `source.accent_colour` | `#rrggbb` or `null` (then the hue of the primary colour turned 330 degrees) |
741
+ | `source.font` | `system`, `inter`, `albert-sans`, `source-sans-3`, `nunito-sans` or `ibm-plex-sans` |
742
+ | `source.corners` | `scherp` (0 px), `normaal` (6 px) or `rond` (12 px) |
743
+ | `source.logo`, `source.logo_dark` | `cube-theme/logo.<ext>` next to the app, or `null` |
744
+ | `tokens` | Optional per-token overrides; only a valid value is used, anything else keeps the derived one |
745
+ | `font_files` | Optional; default the files of the chosen font as `cube-theme/<file>` |
746
+
747
+ The tokens are the CSS custom properties `--cube-<name>` on every block: `color-primary`, `color-primary-hover`,
748
+ `color-primary-subtle`, `color-primary-text`, `color-on-primary`, `color-accent`, `color-on-accent`, `color-text`,
749
+ `color-text-muted`, `color-bg`, `color-surface`, `color-border`, `color-focus`, `color-danger`, `color-success`,
750
+ `color-warning`, `color-example-bg`, `color-example-text`, `color-series-1` to `color-series-6`, `radius`,
751
+ `radius-small` and `font-family`. Text colours are checked for WCAG 2.1 AA contrast: `color-on-primary` and
752
+ `color-on-accent` on their colours, and `color-primary-text` (also the focus ring) at 4.5:1 on both the surface
753
+ `#f6f8fa` and `color-primary-subtle`; a primary colour that falls short is darkened for text in 5% steps (warning
754
+ `primary-darkened-for-text`).
755
+
756
+ A value that could break out of the stylesheet never reaches it: colours must be `#rrggbb`, radii a number of `px`
757
+ or `rem`, and `font-family` a comma-separated list of names (quoted names allowed, but no unbalanced quote, no
758
+ semicolon, brace, backslash or comment). Logo and font addresses are kept only when they are on the page's own
759
+ origin (or a base64 `data:` image) and hold no quote, parenthesis or whitespace. A `data:` logo shows in the chat
760
+ preview, but a hosted app's CSP (`img-src 'self'`) blocks it: a published app uses `cube-theme/logo.<ext>`.
761
+
762
+ The theme sheet sets the tokens on `:where(:root)`, which has no specificity, so an app's own stylesheet can
763
+ override any token in `:root`, for example `:root { --cube-color-primary: #0b5fff; }`.
764
+
765
+ `CubeApp.theme(fileOrSource)` registers a theme from code (a `cube-theme.json` object, or just its `source`) and
766
+ applies it at once; `CubeApp.theme(null)` forgets it. `connect({ theme })` takes the same value, or `false` for
767
+ the neutral theme without reading `cube-theme.json`. The font files ship in the package under `fonts/`; a host
768
+ serves them as `cube-theme/<file>` (for example `fonts/inter.woff2` as `cube-theme/inter.woff2`). Each font has
769
+ its own OFL licence text next to it.
770
+
771
+ ### Blokken
772
+
773
+ Every block is a custom element; the full list with attributes lives in the manifest. Row data is only
774
+ ever rendered as text, never as HTML. Every control works with the keyboard and shows a focus ring, and the
775
+ blocks work from 320 px wide.
776
+
777
+ | Element | Use |
778
+ |---|---|
779
+ | `cube-app-shell` | `name` (required), `logo`, `logo-alt`. Header, user with Inloggen/Uitloggen, page navigation that follows `#/<name>` |
780
+ | `cube-page` | `name` (id in the address), `label` (text in the navigation). A configuration element: it only holds blocks |
781
+ | `cube-state` | `kind`: `loading`, `empty`, `error`, `forbidden`, `signed-out`; `message` replaces the standard text; `retry="false"` leaves out the retry button. Event `cube-retry` |
782
+ | `cube-period-picker` | `default` (`this-month`, `12m`, `this-year`, `custom`), `label`, `today`. Feeds other blocks; `value` is `{ key, from, to, previous, today }`, event `cube-change` |
783
+ | `cube-kpi` | `resource`, `measure` (required), `label`, `format`, `currency`, `filter`, `period`, `date-field`, `compare`. One figure; `compare` adds the change against the previous period (`nieuw` when that was zero) |
784
+ | `cube-chart` | `resource`, `measure`, `by` (required), `kind` (`line`/`bar`), `label`, `format`, `currency`, `filter`, `period`, `date-field`, `lookup`, `height`, `limit`. Own SVG, no chart library; a table for screen readers |
785
+ | `cube-top` | `resource`, `measure`, `by` (required), `limit`, `order`, `label`, `format`, `currency`, `filter`, `period`, `date-field`, `lookup`. Sorted and cut in the browser (an aggregate cannot be sorted by the API). Over 1000 groups the DMP refuses the aggregate (`422 cardinality_exceeded`, reason `too_many_groups`); the block then asks for a shorter period or a filter, without a retry. Event `cube-select` |
786
+ | `cube-field` | `name` (required), `label`, `format` (`text`, `number`, `integer`, `currency`, `percent`, `date`, `datetime`, `boolean`), `multiline`, `options` (`value=Label,...`), `hidden`. A configuration element, a direct child of `cube-table`, `cube-detail`, `cube-form` or `cube-board`; it is never rendered. Live `_capabilities` carries no `format` or `enum`, so a date column or a choice filter there comes from `format`/`options` here or from the values themselves, never from a guess at the type |
787
+ | `cube-table` | `resource` (required), `fields`, `search` (one field, box "Zoeken"), `filters` (a control per field: choices, Alle/Ja/Nee, from-to dates or numbers, else contains), `sort` (`-` for descending), `page-size` (default 25, at most 200), `export`, `detail` (selector of a `cube-detail`), `filter`, `period`, `date-field`. Needs `list`. Search waits 300 ms and the last answer wins. Enter or Space chooses a row, which stays marked (`aria-current`). Event `cube-select` |
788
+ | `cube-detail` | `resource` (required), `record-id`, `fields`, `editable`. Needs `get`, and `update` for `editable`. Events `cube-saved`, `cube-data-change` |
789
+ | `cube-form` | `resource` (required), `fields`, `submit-label` (default "Opslaan"), `success-text` (default "Opgeslagen. Dank je wel!"). Needs `create`. Events `cube-created`, `cube-data-change` |
790
+ | `cube-calc` | `label`. A calculator that needs no data: an editor per `cube-input`, an output per `cube-result`, recalculated on every keystroke by the `formula` property. Read-only `values` and `results`, event `cube-change` (`{ values, results }`) |
791
+ | `cube-input` | `name`, `label` (required), `type` (`number`, `integer`, `currency`, `percent`, `text`, `select`, `checkbox`), `value`, `min`, `max`, `step`, `options` (a select needs it; `value=Label,...` as in `cube-field`). A configuration element, a direct child of `cube-calc`; it is never rendered. Numbers are typed the Dutch way (`7,5`) |
792
+ | `cube-result` | `name`, `label` (required), `format` (`number`, `integer`, `currency`, `percent`, `text`), `currency` (default `EUR`), `emphasis` (the large figure). A configuration element, a direct child of `cube-calc`; it is never rendered |
793
+ | `cube-board` | `resource`, `status-field`, `title-field` (required), `fields` (extra lines on a card), `sort`, `filter`, `page-size` (default 50, at most 200; a column with more says "en nog N"). `cube-field` children label and format the `fields` of a card. Cards in a column per status value. `status-field` must be filterable and `sort` a sortable field. Needs `list`; `update` is optional (without it the board is read-only). Event `cube-move` (`{ resource, id, from, to }`), followed by `cube-data-change` |
794
+ | `cube-column` | `value` (required), `label`. A configuration element, a direct child of `cube-board`, in order; it is never rendered |
795
+
796
+ `cube-table`, `cube-detail` and `cube-form` need the matching actions in the app's `uses`
797
+ (`"orders": ["list", "get", "create", "update"]`). A table sends the row it chose to the `cube-detail` named by
798
+ `detail="#bestelling"`; after a save or a create every block on that resource loads again (`cube-data-change`).
799
+
800
+ - **CSV export** (`export`): the filtered rows, at most 10 000, with `;` as the separator, a UTF-8 BOM and CRLF line
801
+ ends, so a Dutch spreadsheet opens it with the right columns. A text value that starts with `=`, `+`, `-`, `@`, a
802
+ tab or a carriage return gets a `'` in front (formula injection); a genuine number in a numeric column, such as
803
+ `-5` or `12,5`, is not prefixed (in a text column `-5` becomes `'-5`). Pressing the button again while it runs
804
+ cancels.
805
+ - **Filters**: `filter` and the followed period are always applied; the controls can only narrow them. A bound
806
+ takes the stricter of the two (a "Vanaf" before the period start keeps the period start), and a field that
807
+ `filter` fixes to one value gets no control. A `filters` or `search` field the resource cannot be filtered on
808
+ (not in the schema's `filterable`) shows an error naming that field instead of a table.
809
+ - **Required fields**: a field the DMP requires shows " (verplicht)" after its label, besides `aria-required`.
810
+ - **Writes**: every submit attempt carries an `Idempotency-Key` (`newIdempotencyKey()`), the button is disabled
811
+ while saving, and a retry after a network error reuses the key, so a double click creates one row. `cube-detail`
812
+ sends only the changed fields with `ifMatch` set to the `recordVersion` it loaded. When someone else changed the
813
+ row meanwhile (409 or 412) it shows "Iemand anders heeft deze rij intussen gewijzigd" with "Opnieuw laden", keeps
814
+ what you typed and does not retry by itself. Read-only fields are never shown as an input and never sent.
815
+ - **Numbers**: a number field takes Dutch input. `12,5` is sent as `12.5`, `1.234` is one thousand two hundred
816
+ thirty-four (the dot is a thousands separator), `12.5` also works, and `abc` is refused in the browser with
817
+ "Vul een getal in."
818
+
819
+ ```html
820
+ <cube-table resource="orders" fields="reference,placed_on,total,status" search="reference" filters="status,placed_on"
821
+ export detail="#bestelling">
822
+ <cube-field name="total" label="Bedrag" format="currency"></cube-field>
823
+ </cube-table>
824
+ <cube-detail id="bestelling" resource="orders" editable></cube-detail>
825
+ <cube-form resource="orders" fields="reference,placed_on,total,status"></cube-form>
826
+ ```
827
+
828
+ The three data blocks (`cube-kpi`, `cube-chart`, `cube-top`) read aggregates, so the app's `cube-app.json` needs
829
+ `aggregate` in `uses` for each resource they read; `lookup` also needs `list` on the lookup resource
830
+ (`"orders": ["list", "aggregate"], "customers": ["list"]`). The `cube-period-picker` reads nothing itself: it only
831
+ feeds them. `period="#periode"` points at a picker by CSS selector and needs `date-field`, the date the period
832
+ filters on, except on a `cube-chart` whose `by` is a truncated date (`placed_on.month`): that field is used.
833
+ `compare` needs `period`. The period filter is always `{ gte: from, lt: dayAfterTo }` on that field, never
834
+ `between`, so it is right for dates and datetimes alike. A chart leaves the buckets after the picker's `today` empty
835
+ (a gap, not a zero). An aggregate over more than 1000 groups is refused by the DMP; `cube-chart` and `cube-top` then
836
+ say so ("Te veel groepen…") and ask for a shorter period or a filter, without a retry button.
837
+
838
+ ```html
839
+ <cube-period-picker id="periode" default="12m"></cube-period-picker>
840
+ <cube-kpi resource="orders" measure="sum(total)" label="Omzet" format="currency"
841
+ period="#periode" date-field="placed_on" compare></cube-kpi>
842
+ <cube-chart resource="orders" measure="sum(total)" by="placed_on.month" period="#periode"></cube-chart>
843
+ <cube-top resource="orders" measure="sum(total)" by="customer_id" lookup="customers.name"
844
+ period="#periode" date-field="placed_on"></cube-top>
845
+ ```
846
+
847
+ #### Calculator and board
848
+
849
+ - **Formula** (`cube-calc`): `formula` is a synchronous JavaScript function property, set from a script, never an
850
+ attribute (an `async` one returns a Promise, which shows "—"). A string is refused and nothing is evaluated, so it
851
+ works under the CSP. It gets `values` by `cube-input` name and returns an object by `cube-result` name (or one
852
+ number or text when there is a single result):
853
+
854
+ ```js
855
+ document.querySelector('cube-calc').formula = ({ uren, tarief, btw }) => ({
856
+ totaal: uren * tarief * (1 + btw),
857
+ });
858
+ ```
859
+
860
+ A `percent` input is typed as `21` and arrives as the fraction `0.21`; an empty or invalid input arrives as
861
+ `null` (also below `min`, above `max` or off `step`). A result that is `null` or `undefined` just shows "—". A
862
+ formula that throws, or a result that is `NaN` or `Infinity`, shows "—" and one alert; the inputs keep working. Two
863
+ or more `cube-result`s need an object back: a single number or text then shows an error instead of the calculator.
864
+ `cube-change` follows a recalculation by typing or by a new formula on a drawn calculator; not the first draw.
865
+ - **Columns** (`cube-board`): the columns come from the `cube-column` children, in that order. Without them the
866
+ board takes the `enum` of the status field, but live `_capabilities` carries no `enum`, so a starter must always
867
+ write the `cube-column`s (live without any shows an error). Each column loads its own cards, filtered on the
868
+ status field, so that field must be filterable on the resource (and `sort` a sortable field), else the board
869
+ shows an error; a status value without a column is not shown.
870
+ - **Moving**: drag and drop, or a compact button per other column on each card, so it works with the keyboard too.
871
+ A button shows an arrow and the column name ("→ Bezig"); its accessible name is "Verplaats naar Bezig". The card
872
+ moves at once, then is saved (`update` of only the status field); a refusal puts it back with an alert.
873
+ - **Read-only**: without `update` in the app's `uses`, or when the status field is read-only, cards cannot move and
874
+ show no buttons. Columns stack on a narrow screen (from 320 px).
875
+
876
+ ### Wireframe
877
+
878
+ A wireframe is a proposal that shows what an app would look like, partly on real data and partly on invented data.
879
+ The kind is `soort`: `"wireframe"` in `cube-app.json`, copied by the DMP into `cube-app.config.json`. Only a
880
+ wireframe gets any of what follows; a live app without `soort: "wireframe"` never reads `cube-mock.json` and never
881
+ answers from invented data, and a broken server stays an error.
882
+
883
+ What the DMP does not have yet is listed in `ontbreekt` in `cube-app.json`, one entry per missing field or per
884
+ missing resource:
620
885
 
621
- Pin it by copying the built file from a known commit or tag into the host, rather than fetching it
622
- at runtime: `npm pack git+ssh://git@gitlab.cube.nl/cube/packages/laravel/filament/dmp/sdk-js.git#<ref>`
623
- builds the tarball, `package/dist/cube-sdk.js` is the file. Nothing from a CDN, nothing at request
624
- time.
886
+ ```json
887
+ {
888
+ "soort": "wireframe",
889
+ "ontbreekt": [
890
+ { "resource": "orders", "veld": "delivery_note", "type": "tekst", "scherm": "Lijst",
891
+ "reden": "Een opmerking voor de bezorger staat nog niet bij een bestelling." },
892
+ { "resource": "visits", "type": "tekst", "scherm": "Wat ontbreekt",
893
+ "reden": "Bezoeken aan de winkel worden nog nergens vastgelegd." }
894
+ ]
895
+ }
896
+ ```
897
+
898
+ `resource` is a REST resource name, `veld` the missing field (left out when the whole resource is missing), `type`
899
+ one of `tekst`, `getal`, `bedrag`, `datum`, `keuze`, `ja/nee`, `waarden` the choices (required for `keuze`), `scherm`
900
+ the screen that shows it and `reden` why it is needed. The SDK only reads this; `cube/apps` validates it. A
901
+ malformed entry is dropped with one `console.warn`.
902
+
903
+ - **Mixed mode.** What exists runs live. A missing field is added to the live rows with a value invented from the
904
+ row's `id` (the same row shows the same value every time); a missing resource is answered from `cube-mock.json`.
905
+ The live request never names a missing field: a filter or sort on it applies to the returned page only, and an
906
+ aggregate that groups by or sums it is answered from the mock rows. The preview does the same; there, an aggregate
907
+ on a missing field needs that field in the rows of `cube-mock.json`.
908
+ - **Marking.** Blocks mark what is invented: `Voorbeeld — bestaat nog niet`. A table or detail marks the column or
909
+ field; the other blocks mark the whole block.
910
+ - **Simulated writes.** `create`, `update`, `delete`, `operation` and `collectionOperation` are answered in the
911
+ browser, in live and in preview, and forgotten on reload. A block that writes says
912
+ `Voorbeeld — wordt niet opgeslagen`. A wireframe never writes real data: that covers everything that goes through
913
+ the session (`session.resource()` and the blocks). App code that builds its own client with the exported
914
+ `createClient` bypasses it.
915
+ - **Numbers.** KPI and chart numbers do not reflect simulated writes: they come from the rows the session holds
916
+ without them.
917
+ - **`cube-gap-list`.** One card per gap, for the people who build the real thing: what is missing
918
+ (`resource.veld`) as its heading, then Scherm, Soort gegeven and Waarom. It stays readable at 320 px.
919
+ `scherm="Lijst"` shows only that screen's gaps; without gaps it says nothing is missing.
920
+
921
+ ```html
922
+ <cube-page name="wat-ontbreekt" label="Wat ontbreekt">
923
+ <cube-gap-list></cube-gap-list>
924
+ </cube-page>
925
+ ```
926
+
927
+ `npm run visual` serves the pages with the hosted-app CSP; add `?wireframe=1` to see a wireframe with two gaps.
928
+
929
+ ### CSP
930
+
931
+ The blocks run under `default-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; script-src 'self'`,
932
+ the policy of a hosted app. Therefore: no inline `<script>` or `<style>`, no `style` attribute, no `innerHTML`;
933
+ styles are made with `CSSStyleSheet.replaceSync` and `adoptedStyleSheets`. The test suite guards this (a grep of
934
+ the bundle and a walk through the rendered blocks). Your own page must follow the same rule: load `cube-sdk.js`
935
+ and your module with `src`, and keep the logo, the fonts and the mock data on the same origin.
936
+
937
+ To look at the blocks under that policy, run `npm run visual` and open `http://127.0.0.1:4173/`,
938
+ `/?huisstijl=b` and `/?huisstijl=geen`. The server (`scripts/serve-visual.mjs`, bound to 127.0.0.1) sends the
939
+ policy above on every response and serves only `test/visual/`, `dist/cube-sdk.js`, the test mock and the fonts.
625
940
 
626
941
  ## What this SDK deliberately does not do
627
942
 
@@ -636,7 +951,6 @@ time.
636
951
  - **No token revocation call**: `cube/oauth-server` publishes no revocation endpoint. `signOut()`
637
952
  forgets locally; the person revokes the connection in the DMP under *Verbonden apps*.
638
953
  - **No `_history`**: the API has not built it. `request()` reaches any path once it exists.
639
- - **Not published** to any registry yet.
640
954
 
641
955
  ## Development
642
956
 
@@ -654,6 +968,14 @@ then says what the SDK still reads wrong.
654
968
  TypeScript and esbuild (which bundles `dist/cube-sdk.js`, see `scripts/build-browser.mjs`) are the only
655
969
  dev dependencies; there are no runtime dependencies, and there should not be.
656
970
 
971
+ ### Releasing
972
+
973
+ Bump `version` in `package.json` and `src/version.ts` (a test holds them equal), add the CHANGELOG
974
+ entry, merge, then push a tag that equals the version (e.g. `1.1.0-rc.1`). The tag pipeline's
975
+ `publish` job checks that, builds, and runs `npm publish`: a version with a `-` goes to the `next`
976
+ dist-tag, any other to `latest`. It needs the masked, protected CI variable `NPM_TOKEN` (an npm
977
+ granular access token with write access to the `cube-nl` org).
978
+
657
979
  ## Integration tests
658
980
 
659
981
  `test/integration.test.js` runs the SDK against a real DMP. `npm test` includes it, and it skips
package/dist/app.d.ts CHANGED
@@ -2,6 +2,9 @@ import { type AuthorizationServerMetadata } from './auth.js';
2
2
  import { type ResourceClient } from './client.js';
3
3
  import { type AppManifest, type MockData } from './preview.js';
4
4
  import type { DmpRecord } from './types.js';
5
+ import { type ResourceSchema } from './ui/schema.js';
6
+ import { type ThemeFile, type ThemeSource } from './ui/theme.js';
7
+ import { type GapEntry } from './wireframe.js';
5
8
  /**
6
9
  * `cube-app.config.json`, which the DMP writes next to the app only when it publishes an approved
7
10
  * release. Never part of what the maker or an AI sends: its presence is what makes an app live.
@@ -21,6 +24,8 @@ export interface CubeAppConfig {
21
24
  issuer?: string;
22
25
  /** Passed to `createOAuth`: endpoints to use instead of (or on top of) discovery. */
23
26
  metadata?: Partial<AuthorizationServerMetadata>;
27
+ /** `'wireframe'`: what exists runs live, what `ontbreekt` in cube-app.json lists comes from `cube-mock.json`. */
28
+ soort?: string;
24
29
  }
25
30
  export interface ConnectOptions {
26
31
  /** Preview data: `cube.mock_data`'s output. Default: what `CubeApp.mock()` registered, else `./cube-mock.json`. */
@@ -40,6 +45,11 @@ export interface ConnectOptions {
40
45
  tokenStorage?: 'session' | 'memory';
41
46
  /** Live only: go to the DMP's sign-in right away when nobody is signed in. Default false. */
42
47
  autoSignIn?: boolean;
48
+ /**
49
+ * The house style. Default: what `CubeApp.theme()` registered, else `./cube-theme.json`, else the
50
+ * neutral theme. `false`: neutral, and `./cube-theme.json` is not read.
51
+ */
52
+ theme?: ThemeFile | ThemeSource | false;
43
53
  }
44
54
  export interface CubeAppUser {
45
55
  /** The DMP's user id (the token's `sub`), or `'preview'`. Null when the token does not say. */
@@ -62,6 +72,16 @@ export interface CubeAppSession {
62
72
  resource<T extends DmpRecord = DmpRecord>(name: string): ResourceClient<T>;
63
73
  /** Why the last sign-in came back without a session (consent refused, say); null otherwise. */
64
74
  readonly signInError: Error | null;
75
+ /** One resource as a block sees it: fields, actions, what may be filtered, sorted, grouped. Null when unknown. */
76
+ schema(resource: string): Promise<ResourceSchema | null>;
77
+ /** What the app is: live the config's `soort`, in the preview the manifest's. Null when neither says. */
78
+ readonly soort: string | null;
79
+ /** A wireframe's gap entries: what it shows that the DMP does not have yet. Empty for any other app. */
80
+ readonly gaps: readonly GapEntry[];
81
+ /** Whether a wireframe shows invented data here: the whole resource, or this field of it, does not exist yet. */
82
+ isExample(resource: string, field?: string): boolean;
83
+ /** A wireframe: writes are answered in the browser and never saved. */
84
+ readonly simulatesWrites: boolean;
65
85
  }
66
86
  /**
67
87
  * Starts the app. Reads `./cube-app.config.json` next to the page: absent, the app runs in the
@@ -69,17 +89,31 @@ export interface CubeAppSession {
69
89
  * OAuth (authorization code + PKCE) and talks to the DMP's REST face. The app's code is the same.
70
90
  */
71
91
  declare function connect(options?: ConnectOptions): Promise<CubeAppSession>;
92
+ /**
93
+ * The session `connect()` made for this page, or one with the defaults when the app never connected. The default one
94
+ * starts only once the page has loaded (and one task later), so an app that awaits something before it calls
95
+ * `connect()` still gets there first and the page signs in once.
96
+ */
97
+ declare function ready(): Promise<CubeAppSession>;
72
98
  /**
73
99
  * Registers preview data before `connect()`, for an app that embeds its mock data in a script tag
74
100
  * instead of shipping `cube-mock.json`. Has no effect on a live app. `null` forgets it again.
75
101
  */
76
102
  declare function mock(data: MockData | null): void;
103
+ /**
104
+ * Registers the house style before `connect()`, for an app that holds it in code instead of shipping
105
+ * `cube-theme.json`, and applies it right away. `null` forgets it again (neutral).
106
+ */
107
+ declare function theme(input: ThemeFile | ThemeSource | null): void;
77
108
  /** What `cube-sdk.js` puts on `window`, and what this package exports as `CubeApp`. */
78
109
  export declare const CubeApp: Readonly<{
79
110
  connect: typeof connect;
111
+ /** The session the app connected, or one connected with the defaults. What blocks use. */
112
+ ready: typeof ready;
80
113
  mock: typeof mock;
81
- /** This SDK's version, e.g. `'1.0.0-rc.3'`. */
82
- version: "1.0.0-rc.3";
114
+ theme: typeof theme;
115
+ /** This SDK's version, e.g. `'1.1.0-rc.1'`. */
116
+ version: "1.1.0-rc.1";
83
117
  /** The manifest `"sdk"` value this build serves. */
84
118
  sdk: "1";
85
119
  }>;