@mmlogic/components 0.1.9 → 0.1.11

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 (45) hide show
  1. package/README.md +196 -61
  2. package/dist/cjs/format-DBr-GTvS.js +308 -0
  3. package/dist/cjs/loader.cjs.js +1 -1
  4. package/dist/cjs/mosterdcomponents.cjs.js +1 -1
  5. package/dist/cjs/mrd-boolean-field_16.cjs.entry.js +108 -117
  6. package/dist/cjs/mrd-table.cjs.entry.js +318 -62
  7. package/dist/collection/components/mrd-field/mrd-field.js +26 -2
  8. package/dist/collection/components/mrd-file-field/mrd-file-field.js +70 -2
  9. package/dist/collection/components/mrd-file-field/mrd-file-field.scss +13 -0
  10. package/dist/collection/components/mrd-form/mrd-form.js +28 -1
  11. package/dist/collection/components/mrd-image-field/mrd-image-field.js +71 -2
  12. package/dist/collection/components/mrd-image-field/mrd-image-field.scss +26 -2
  13. package/dist/collection/components/mrd-table/mrd-table.js +386 -62
  14. package/dist/collection/components/mrd-table/mrd-table.scss +388 -0
  15. package/dist/collection/dev/app.js +48 -4
  16. package/dist/collection/dev/sprites.svg +55 -0
  17. package/dist/collection/utils/i18n.js +144 -0
  18. package/dist/components/i18n.js +1 -1
  19. package/dist/components/mrd-field2.js +1 -1
  20. package/dist/components/mrd-file-field2.js +1 -1
  21. package/dist/components/mrd-form.js +1 -1
  22. package/dist/components/mrd-image-field2.js +1 -1
  23. package/dist/components/mrd-table.js +1 -1
  24. package/dist/esm/format-EzhfM0uw.js +299 -0
  25. package/dist/esm/loader.js +1 -1
  26. package/dist/esm/mosterdcomponents.js +1 -1
  27. package/dist/esm/mrd-boolean-field_16.entry.js +82 -91
  28. package/dist/esm/mrd-table.entry.js +318 -62
  29. package/dist/mosterdcomponents/mosterdcomponents.esm.js +1 -1
  30. package/dist/mosterdcomponents/p-27f6947a.entry.js +1 -0
  31. package/dist/mosterdcomponents/p-EzhfM0uw.js +1 -0
  32. package/dist/mosterdcomponents/p-ca5f9919.entry.js +1 -0
  33. package/dist/types/components/mrd-field/mrd-field.d.ts +5 -0
  34. package/dist/types/components/mrd-file-field/mrd-file-field.d.ts +10 -0
  35. package/dist/types/components/mrd-form/mrd-form.d.ts +5 -0
  36. package/dist/types/components/mrd-image-field/mrd-image-field.d.ts +10 -0
  37. package/dist/types/components/mrd-table/mrd-table.d.ts +52 -18
  38. package/dist/types/components.d.ts +53 -3
  39. package/dist/types/utils/cell-renderer.d.ts +27 -0
  40. package/package.json +1 -1
  41. package/dist/cjs/format-CDw-zie_.js +0 -82
  42. package/dist/esm/format-Dt-aHxkM.js +0 -74
  43. package/dist/mosterdcomponents/p-2a8cb2eb.entry.js +0 -1
  44. package/dist/mosterdcomponents/p-Dt-aHxkM.js +0 -1
  45. package/dist/mosterdcomponents/p-baf08615.entry.js +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @mmlogic/components
2
2
 
3
- A **Stencil.js v4** web component library for rendering dynamic forms and paginated data tables from API descriptors. Components are framework-agnostic and work in any HTML/JS environment. An Angular output target is included.
3
+ A **Stencil.js v4** web component library for rendering dynamic forms and paginated data tables from API descriptors. Components are framework-agnostic (plain HTML/JS) and an Angular output target is included.
4
4
 
5
5
  ## Installation
6
6
 
@@ -13,7 +13,8 @@ npm install @mmlogic/components
13
13
  ### Vanilla HTML / JS
14
14
 
15
15
  ```html
16
- <script type="module" src="node_modules/@mmlogic/components/dist/mosterd-components/mosterd-components.esm.js"></script>
16
+ <link rel="stylesheet" href="node_modules/@mmlogic/components/dist/mosterdcomponents/mosterdcomponents.css" />
17
+ <script type="module" src="node_modules/@mmlogic/components/dist/mosterdcomponents/mosterdcomponents.esm.js"></script>
17
18
 
18
19
  <mrd-form id="my-form"></mrd-form>
19
20
 
@@ -28,7 +29,8 @@ npm install @mmlogic/components
28
29
  ### Via CDN (unpkg)
29
30
 
30
31
  ```html
31
- <script type="module" src="https://unpkg.com/@mmlogic/components/dist/mosterd-components/mosterd-components.esm.js"></script>
32
+ <link rel="stylesheet" href="https://unpkg.com/@mmlogic/components/dist/mosterdcomponents/mosterdcomponents.css" />
33
+ <script type="module" src="https://unpkg.com/@mmlogic/components/dist/mosterdcomponents/mosterdcomponents.esm.js"></script>
32
34
  ```
33
35
 
34
36
  ### Angular
@@ -50,45 +52,118 @@ export class AppModule {}
50
52
 
51
53
  ### `<mrd-form>`
52
54
 
53
- Renders a complete form from a layout descriptor. Handles validation, RTL layout (Arabic locale), and emits the submitted values.
55
+ Renders a complete form from a layout descriptor. Handles validation, RTL layout (Arabic locale), and orchestrates file uploads, relation search, and dependent dropdowns.
54
56
 
55
57
  ```html
56
- <mrd-form id="form" locale="nl-NL"></mrd-form>
58
+ <mrd-form id="form" locale="nl-NL" show-cancel></mrd-form>
57
59
  ```
58
60
 
59
61
  ```js
62
+ const form = document.getElementById('form');
63
+
60
64
  form.layout = {
61
- title: 'New order',
65
+ title: 'New invoice',
62
66
  items: [
63
- { type: 'FIELD', field: { name: 'name', label: 'Name', dataType: 'TEXT', required: true } },
67
+ { type: 'FIELD', field: { name: 'description', label: 'Description', dataType: 'TEXT', required: true } },
64
68
  { type: 'FIELD', field: { name: 'amount', label: 'Amount', dataType: 'CURRENCY' } },
65
69
  ]
66
70
  };
67
- form.addEventListener('mrdSubmit', e => console.log(e.detail.values));
68
- form.addEventListener('mrdCancel', () => console.log('cancelled'));
71
+
72
+ form.addEventListener('mrdSubmit', e => console.log(e.detail));
73
+ form.addEventListener('mrdCancel', () => history.back());
69
74
  ```
70
75
 
71
- **Props**
76
+ #### Props
72
77
 
73
- | Prop | Type | Description |
74
- |---|---|---|
75
- | `layout` | `ClientLayout` | Form descriptor with title and items |
76
- | `values` | `Record<string, any>` | Initial field values |
77
- | `locale` | `string` | BCP 47 locale (default: `navigator.language`) |
78
- | `disabled` | `boolean` | Disables all inputs |
78
+ | Prop | Type | Default | Description |
79
+ |---|---|---|---|
80
+ | `layout` | `ClientLayout` | required | Form descriptor with `title` and `items[]` |
81
+ | `values` | `Record<string, any>` | `{}` | Initial field values; re-setting this prop re-fills the form (edit mode) |
82
+ | `locale` | `string` | `navigator.language` | BCP 47 locale for labels, validation messages, and formatting |
83
+ | `referenceHref` | `string` | `''` | Absolute href of a parent/context object. Combined with `referenceClass`, `mrd-form` automatically pre-fills the matching relation field so dependent dropdowns load without any host-side form logic. |
84
+ | `referenceClass` | `string` | `''` | `mostSignificantClass` of the parent object (e.g. `'clientAgreements'`). Used to locate the relation field to pre-fill. |
85
+ | `showCancel` | `boolean` | `false` | When `true`, shows a Cancel button next to Submit in the footer. |
79
86
 
80
- **Events**
87
+ #### Events
81
88
 
82
89
  | Event | Detail | Description |
83
90
  |---|---|---|
84
- | `mrdSubmit` | `{ values: Record<string, any> }` | Form submitted and valid |
85
- | `mrdCancel` | — | Cancel button clicked |
91
+ | `mrdSubmit` | `Record<string, any>` | Fired on valid submit. Payload contains only layout fields; relation values are normalised to plain href strings. |
92
+ | `mrdCancel` | — | Fired when the Cancel button is clicked. Host handles navigation. |
93
+ | `mrdSearch` | `{ query: string; relatedClass: string }` | Relation search requested. Host calls `field.setSearchResults(results)` with the API response. |
94
+ | `mrdFetchAll` | see below | All records requested for a DROPDOWN relation. Host calls `field.setAllRecords(results)`. |
95
+ | `mrdUpload` | `{ name: string; file: File }` | A file was selected. Host uploads the file and calls `form.setFieldValue(name, uri)` when done. The field shows a loading spinner until the URI arrives. |
96
+
97
+ #### Methods
98
+
99
+ | Method | Description |
100
+ |---|---|
101
+ | `setFieldValue(name, value)` | Inject a value into a named field from outside (e.g. after a file upload completes). |
102
+ | `setSearchResults(results)` | *(on `mrd-relation-field`)* Populate the search autocomplete list. |
103
+ | `setAllRecords(results)` | *(on `mrd-relation-field`)* Populate a DROPDOWN `<select>` with all records. |
104
+
105
+ #### `mrdFetchAll` event detail
106
+
107
+ ```ts
108
+ {
109
+ name: string; // field name — use to find the mrd-relation-field element
110
+ relatedClass: string; // dot-notation class (e.g. "legalmanager.budget")
111
+ mostSignificantClass?: string; // URL segment (e.g. "budgets")
112
+ commonRelation?: string; // name of the dependency field (for dependent dropdowns)
113
+ filter?: string; // query param name
114
+ filterValue?: string; // href of the selected dependency; empty string = clear list
115
+ }
116
+ ```
117
+
118
+ #### Full integration example
119
+
120
+ ```js
121
+ // mrdSearch — typeahead for RELATION fields
122
+ form.addEventListener('mrdSearch', async (e) => {
123
+ const { query, relatedClass } = e.detail;
124
+ const data = await fetch(`/api/search/${relatedClass}?q=${query}`).then(r => r.json());
125
+ const field = form.querySelector(`mrd-relation-field[name="${relatedClass}"]`);
126
+ field?.setSearchResults(data.map(r => ({ id: r._links.self.href, label: r.name })));
127
+ });
128
+
129
+ // mrdFetchAll — populate DROPDOWN selects (including dependent dropdowns)
130
+ form.addEventListener('mrdFetchAll', async (e) => {
131
+ const { name, mostSignificantClass, filter, filterValue } = e.detail;
132
+ const field = form.querySelector(`mrd-relation-field[name="${name}"]`);
133
+ if (!field) return;
134
+
135
+ if (filter && !filterValue) { // dependency cleared → empty the list
136
+ field.setAllRecords([]);
137
+ return;
138
+ }
139
+
140
+ let url = `/data/${tenantId}/${mostSignificantClass}`;
141
+ if (filter && filterValue) url += `?${filter}=${encodeURIComponent(filterValue)}`;
142
+ const result = await fetch(url).then(r => r.json());
143
+ const records = Object.values(result._embedded ?? {})[0] ?? [];
144
+ field.setAllRecords(records.map(r => ({ id: r._links.self.href, label: r.name })));
145
+ });
146
+
147
+ // mrdUpload — stream file to server before submit
148
+ form.addEventListener('mrdUpload', async (e) => {
149
+ const { name, file } = e.detail;
150
+ const formData = new FormData();
151
+ formData.append('file', file);
152
+
153
+ // Get pre-signed upload URL from server, then POST the file
154
+ const uploadUrl = await fetch('/api/upload').then(r => r.json());
155
+ const [uri] = await fetch(uploadUrl, { method: 'POST', body: formData }).then(r => r.json());
156
+
157
+ // Return the stored URI to the field — spinner disappears, value is ready for submit
158
+ form.setFieldValue(name, uri);
159
+ });
160
+ ```
86
161
 
87
162
  ---
88
163
 
89
164
  ### `<mrd-table>`
90
165
 
91
- A virtual-scroll paginated table. Only the visible rows (plus a configurable buffer) are rendered in the DOM. Pages are fetched on demand via an event.
166
+ A virtual-scroll paginated table with column sorting, Excel-style filtering, toolbar actions, and a pagination footer. Only visible rows are in the DOM; pages are fetched on demand via an event.
92
167
 
93
168
  ```html
94
169
  <mrd-table id="table"></mrd-table>
@@ -97,60 +172,105 @@ A virtual-scroll paginated table. Only the visible rows (plus a configurable buf
97
172
  ```js
98
173
  const table = document.getElementById('table');
99
174
 
100
- table.columns = view.values; // TableColumn[] from dashboard API
175
+ table.columns = view.columns; // TableColumn[]
101
176
  table.totalElements = page.totalElements;
102
177
  table.pageSize = page.size;
103
- table.tableHeight = 500; // px, fixed container height
104
- table.locale = 'nl-NL';
178
+ table.tableHeight = 500; // px, fixed container height
179
+ table.locale = 'nl-NL'; // en / nl / ar / fr
105
180
  table.defaultSort = view.defaultSort ?? '';
181
+ table.actions = [
182
+ { action: 'create', label: 'New record', icon: 'assets/sprites.svg#icon-plus', variant: 'primary' },
183
+ { action: 'export', label: 'Export to Excel', icon: 'assets/sprites.svg#icon-file-excel', disabled: true },
184
+ ];
185
+
186
+ let activeFilters = [];
187
+
188
+ table.addEventListener('mrdFilter', (e) => {
189
+ activeFilters = e.detail.filters; // ColumnFilter[]
190
+ });
106
191
 
107
192
  table.addEventListener('mrdLoadPage', async (e) => {
108
193
  const { page, sort } = e.detail;
109
- const url = `${dataHref}?page=${page}${sort ? '&sort=' + sort : ''}`;
110
- const result = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }).then(r => r.json());
111
- const rows = Object.values(result._embedded ?? {})[0] ?? [];
194
+ const params = new URLSearchParams();
195
+ if (page > 0) params.set('page', String(page));
196
+ if (sort) params.set('sort', sort);
197
+ activeFilters.forEach(f => {
198
+ if (f.operator === 'isEmpty') { params.set(f.field, ''); return; }
199
+ if (f.operator === 'isNotEmpty') { params.set(f.field + '_notempty', 'true'); return; }
200
+ if (f.values?.length) { params.set(f.field, f.values.join(',')); return; }
201
+ if (f.value != null) params.set(f.field, String(f.value));
202
+ if (f.from != null) params.set(f.field + '_from', String(f.from));
203
+ if (f.to != null) params.set(f.field + '_to', String(f.to));
204
+ });
205
+ const qs = params.toString();
206
+ const result = await fetch(qs ? `${dataUrl}?${qs}` : dataUrl).then(r => r.json());
207
+ const rows = Object.values(result._embedded ?? {})[0] ?? [];
112
208
  await table.setPage(page, rows);
113
209
  });
114
210
 
211
+ table.addEventListener('mrdAction', (e) => {
212
+ if (e.detail.action === 'create') location.href = '/new';
213
+ if (e.detail.action === 'export') window.open(excelUrl, '_blank');
214
+ });
215
+
216
+ table.addEventListener('mrdRowClick', (e) => {
217
+ location.href = e.detail._links.self.href;
218
+ });
219
+
115
220
  await table.init();
116
- await table.setPage(0, page0Rows); // inject pre-fetched page 0
221
+ await table.setPage(0, page0Rows); // inject pre-fetched page 0 without a second request
117
222
  ```
118
223
 
119
- **Props**
224
+ #### Props
120
225
 
121
226
  | Prop | Type | Default | Description |
122
227
  |---|---|---|---|
123
228
  | `columns` | `TableColumn[]` | `[]` | Column definitions |
124
- | `totalElements` | `number` | `0` | Total records (`0` = non-paginated mode using `rows` prop) |
125
- | `pageSize` | `number` | `20` | Records per page |
229
+ | `totalElements` | `number` | `0` | Total record count. `0` = non-paginated mode (use `rows` prop instead) |
230
+ | `pageSize` | `number` | `20` | Records per page (must match API page size) |
126
231
  | `rowHeight` | `number` | `36` | Row height in px |
127
- | `tableHeight` | `number` | `500` | Container height in px |
128
- | `locale` | `string` | `navigator.language` | Locale for cell formatting |
232
+ | `tableHeight` | `number` | `500` | Fixed scroll-container height in px |
233
+ | `locale` | `string` | `navigator.language` | Locale for cell formatting and built-in labels (`en`/`nl`/`ar`/`fr`) |
129
234
  | `defaultSort` | `string` | `''` | Initial sort, e.g. `"timestamp,desc"` or `"name"` |
130
235
  | `rows` | `Record[]` | `[]` | Rows for non-paginated mode |
236
+ | `actions` | `TableAction[]` | `[]` | Toolbar action buttons (`{ action, label, icon?, variant?, disabled? }`) |
131
237
 
132
- **Events**
238
+ #### Events
133
239
 
134
240
  | Event | Detail | Description |
135
241
  |---|---|---|
136
242
  | `mrdLoadPage` | `{ page: number; sort: string }` | Fired when a page needs to be fetched |
243
+ | `mrdRowClick` | row object | Fired when a data row is clicked |
244
+ | `mrdAction` | `{ action: string }` | Fired when a toolbar action button is clicked |
245
+ | `mrdFilter` | `{ filters: ColumnFilter[] }` | Fired when active column filters change |
137
246
 
138
- **Methods**
247
+ #### Methods
139
248
 
140
249
  | Method | Description |
141
250
  |---|---|
142
- | `init()` | Reset state and render window; call after props are set and before `setPage(0, …)` |
143
- | `setPage(page, rows)` | Inject fetched rows for a page number |
251
+ | `init()` | Reset state and render window. Call after all props are set and before `setPage(0, …)`. |
252
+ | `setPage(page, rows)` | Inject fetched rows for a page. Automatically clamps the render window when the page is shorter than `pageSize` — prevents shimmer rows beyond actual data. |
144
253
 
145
- **Column sorting**
254
+ **Sort format:** `"fieldName"` for ASC, `"fieldName,desc"` for DESC — append directly as `&sort=<value>`.
146
255
 
147
- Click any column header to sort. Clicking again toggles ASC ↔ DESC. The active column is highlighted and shows ▲ / ▼. The `sort` value in the `mrdLoadPage` event is the raw query-param value (`"fieldname"` or `"fieldname,desc"`).
256
+ #### Column filtering
257
+
258
+ A filter-toggle button (▼) in the toolbar activates filter mode. Clicking ▾ in any column header opens a popup with sort and type-specific filter controls. Supported per data type:
259
+
260
+ | DataType | Filter |
261
+ |---|---|
262
+ | TEXT, EMAIL, HYPERLINK, RELATION | Starts with / equals / is empty / is not empty |
263
+ | INTEGER, DECIMAL, PERCENTAGE, CURRENCY | Exact value or from–to range |
264
+ | DATE, DATETIME, TIME | Exact date or from–to range |
265
+ | BOOLEAN | All / Yes / No |
266
+ | LIST | Checkbox list |
267
+ | FILE, IMAGE | Not supported |
148
268
 
149
269
  ---
150
270
 
151
271
  ### Field components
152
272
 
153
- Individual input components can be used standalone. Each emits `mrdChange` and `mrdBlur`.
273
+ All leaf components can be used standalone. Each emits `mrdChange` and `mrdBlur`.
154
274
 
155
275
  | Component | DataType | Description |
156
276
  |---|---|---|
@@ -165,53 +285,68 @@ Individual input components can be used standalone. Each emits `mrdChange` and `
165
285
  | `<mrd-email-field>` | `EMAIL` | Email input with validation |
166
286
  | `<mrd-hyperlink-field>` | `HYPERLINK` | URL input with validation |
167
287
  | `<mrd-list-field>` | `LIST` | Single dropdown or multi-checkbox |
168
- | `<mrd-file-field>` | `FILE` | Drag-and-drop file upload |
169
- | `<mrd-image-field>` | `IMAGE` | Drag-and-drop image upload with preview |
170
- | `<mrd-relation-field>` | `RELATION` | Search (event-driven) or dropdown |
288
+ | `<mrd-file-field>` | `FILE` | Drag-and-drop file upload with upload spinner |
289
+ | `<mrd-image-field>` | `IMAGE` | Drag-and-drop image upload with local preview and spinner |
290
+ | `<mrd-relation-field>` | `RELATION` | Typeahead search (event-driven) or DROPDOWN select |
171
291
 
172
- **Common props for field components**
292
+ #### Common props
173
293
 
174
294
  | Prop | Type | Description |
175
295
  |---|---|---|
176
- | `name` | `string` | Field name, included in `mrdChange` detail |
296
+ | `name` | `string` | Field name, echoed in event detail |
177
297
  | `label` | `string` | Visible label |
178
298
  | `value` | `any` | Current value |
179
- | `required` | `boolean` | Shows validation error when empty on blur |
299
+ | `required` | `boolean` | Validates on blur; shows error when empty |
180
300
  | `disabled` | `boolean` | Disables the input |
181
- | `locale` | `string` | BCP 47 locale for formatting |
301
+ | `locale` | `string` | BCP 47 locale |
182
302
 
183
- **Events for field components**
303
+ #### Common events
184
304
 
185
- | Event | Detail |
186
- |---|---|
187
- | `mrdChange` | `{ name: string; value: any }` |
188
- | `mrdBlur` | `{ name: string; value: any }` |
305
+ | Event | Detail | Description |
306
+ |---|---|---|
307
+ | `mrdChange` | `{ name: string; value: any }` | Fired on every value change |
308
+ | `mrdBlur` | `{ name: string; value: any }` | Fired on blur (triggers validation) |
309
+ | `mrdUpload` | `{ name: string; file: File }` | *(file/image only)* Fired immediately when a file is selected so the host can stream it to the server |
310
+
311
+ #### File / image upload flow
312
+
313
+ When a file is picked:
314
+ 1. The field emits `mrdUpload` with the raw `File` object.
315
+ 2. The field immediately shows a spinner and updates its value to the `File` (blocking submit).
316
+ 3. The host uploads the file and calls `form.setFieldValue(name, uri)` (or `field.value = uri`).
317
+ 4. The field detects the string URI, clears the spinner, and is ready for submit.
318
+
319
+ The submit payload always contains the final URI string — never a `File` object.
189
320
 
190
321
  ---
191
322
 
192
323
  ## Theming
193
324
 
194
- All visual properties are CSS custom properties. Override them on `:root` or any ancestor element:
325
+ All design tokens are CSS custom properties. The default primary colour is **green** (`#16a34a`). Override on `:root` or any ancestor to match your brand:
195
326
 
196
327
  ```css
197
328
  :root {
198
- --mrd-color-primary: #2563eb;
199
- --mrd-color-neutral-100: #f3f4f6;
200
- --mrd-border-radius: 0.375rem;
201
- --mrd-font-size-sm: 0.875rem;
202
- /* see full list in src/global/variables.scss */
329
+ --mrd-color-primary: #16a34a; /* green-600 — change to your brand colour */
330
+ --mrd-color-primary-light: #dcfce7;
331
+ --mrd-color-primary-dark: #15803d;
332
+ --mrd-color-primary-hover: #166534;
333
+ --mrd-shadow-focus: 0 0 0 3px rgb(22 163 74 / 0.2); /* match primary */
334
+
335
+ --mrd-color-neutral-100: #f3f4f6;
336
+ --mrd-border-radius: 0.375rem;
337
+ --mrd-font-size-sm: 0.875rem;
203
338
  }
204
339
  ```
205
340
 
341
+ The full list is defined in `src/global/variables.scss` and exposed in the built file `dist/mosterdcomponents/mosterdcomponents.css`.
342
+
206
343
  ---
207
344
 
208
345
  ## TypeScript types
209
346
 
210
- Types are exported from the package root:
211
-
212
347
  ```ts
213
- import type { TableColumn } from '@mmlogic/components/dist/types/utils/cell-renderer';
214
- import type { ClientLayout, ClientLayoutItem } from '@mmlogic/components/dist/types/types/client-layout';
348
+ import type { ClientLayout, ClientLayoutItem, RelationSearchResult } from '@mmlogic/components';
349
+ import type { TableColumn, TableAction, ColumnFilter } from '@mmlogic/components/dist/types/utils/cell-renderer';
215
350
  ```
216
351
 
217
352
  ---