@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
package/README.md CHANGED
@@ -29,6 +29,7 @@ The rest of this document is in English.
29
29
  - [Errors](#errors)
30
30
  - [Voor AI-tools](#voor-ai-tools)
31
31
  - [Route B: apps without a build step](#route-b-apps-without-a-build-step)
32
+ - [Bouwstenen (UI)](#bouwstenen-ui)
32
33
  - [What this SDK deliberately does not do](#what-this-sdk-deliberately-does-not-do)
33
34
  - [Development](#development)
34
35
  - [Integration tests](#integration-tests)
@@ -49,9 +50,9 @@ For an app without a build step, the single-file browser build is on the npm CDN
49
50
  version:
50
51
 
51
52
  ```html
52
- <script src="https://cdn.jsdelivr.net/npm/@cube-nl/dmp-sdk@1.0.0-rc.4/dist/cube-sdk.js"></script>
53
+ <script src="https://cdn.jsdelivr.net/npm/@cube-nl/dmp-sdk@1.1.0-rc.1/dist/cube-sdk.js"></script>
53
54
  <!-- or -->
54
- <script src="https://unpkg.com/@cube-nl/dmp-sdk@1.0.0-rc.4/dist/cube-sdk.js"></script>
55
+ <script src="https://unpkg.com/@cube-nl/dmp-sdk@1.1.0-rc.1/dist/cube-sdk.js"></script>
55
56
  ```
56
57
 
57
58
  Fallback, for an unreleased commit: `npm install
@@ -616,7 +617,7 @@ Without them the preview is permissive: every field, every function; only a fiel
616
617
  |---|---|
617
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`) |
618
619
  | `CubeApp.mock(data)` | Registers preview data for the next `connect()`; `null` forgets it |
619
- | `CubeApp.version` | This SDK's version, e.g. `'1.0.0-rc.4'` |
620
+ | `CubeApp.version` | This SDK's version, e.g. `'1.1.0-rc.1'` |
620
621
  | `CubeApp.sdk` | The manifest `"sdk"` this build serves, `'1'` |
621
622
  | `session.mode` | `'preview'` or `'live'` |
622
623
  | `session.user()` | `{ id, name, scopes }` or `null` |
@@ -624,6 +625,11 @@ Without them the preview is permissive: every field, every function; only a fiel
624
625
  | `session.signOut()` | Forgets the tokens in this browser (not a revocation). Preview: signs the stand-in out |
625
626
  | `session.resource(name)` | A `ResourceClient`: `list`, `pages`, `all`, `get`, `aggregate`, `create`, `update`, `delete`, `operation`, `collectionOperation` |
626
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 |
627
633
 
628
634
  Live, `connect()` handles the OAuth callback itself (on the `redirectUri`), so the redirect URI can
629
635
  be the app's own `index.html`. The tokens go to `sessionStorage` by default, unlike `createOAuth`'s
@@ -639,13 +645,299 @@ app with a build (route A).
639
645
  | | |
640
646
  |---|---|
641
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 |
642
- | Size | 31.4 KiB minified, 11.3 KiB gzipped (1.0.0-rc.4) |
643
- | Version | The banner's first line, `/*! @cube-nl/dmp-sdk 1.0.0-rc.4 cube-sdk.js … */`, and `CubeApp.version` |
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` |
644
650
 
645
651
  Pin it by copying the built file of a known version into the host, rather than fetching it at
646
652
  runtime: `npm pack @cube-nl/dmp-sdk@<version>` downloads the tarball, `package/dist/cube-sdk.js` is
647
653
  the file. Nothing from a CDN, nothing at request time.
648
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:
885
+
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.
940
+
649
941
  ## What this SDK deliberately does not do
650
942
 
651
943
  - **No per-resource generated types.** `_capabilities` is per token and at runtime, and OpenAPI
@@ -679,7 +971,7 @@ dev dependencies; there are no runtime dependencies, and there should not be.
679
971
  ### Releasing
680
972
 
681
973
  Bump `version` in `package.json` and `src/version.ts` (a test holds them equal), add the CHANGELOG
682
- entry, merge, then push a tag that equals the version (e.g. `1.0.0-rc.4`). The tag pipeline's
974
+ entry, merge, then push a tag that equals the version (e.g. `1.1.0-rc.1`). The tag pipeline's
683
975
  `publish` job checks that, builds, and runs `npm publish`: a version with a `-` goes to the `next`
684
976
  dist-tag, any other to `latest`. It needs the masked, protected CI variable `NPM_TOKEN` (an npm
685
977
  granular access token with write access to the `cube-nl` org).
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.4'`. */
82
- version: "1.0.0-rc.4";
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
  }>;