@cratis/scene.blueprint.default 4.38.0 → 4.40.0

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 (167) hide show
  1. package/ComponentName.ts +27 -0
  2. package/defaultBlueprintComponents.ts +10 -1
  3. package/dist/cjs/ComponentName.js +9 -0
  4. package/dist/cjs/ComponentName.js.map +1 -1
  5. package/dist/cjs/defaultBlueprintComponents.js +19 -1
  6. package/dist/cjs/defaultBlueprintComponents.js.map +1 -1
  7. package/dist/cjs/foundation/BulkBar.js +62 -0
  8. package/dist/cjs/foundation/BulkBar.js.map +1 -0
  9. package/dist/cjs/foundation/DataList.js +436 -0
  10. package/dist/cjs/foundation/DataList.js.map +1 -0
  11. package/dist/cjs/foundation/FormScreenBody.js +447 -0
  12. package/dist/cjs/foundation/FormScreenBody.js.map +1 -0
  13. package/dist/cjs/foundation/FoundationStates.js +2 -1
  14. package/dist/cjs/foundation/FoundationStates.js.map +1 -1
  15. package/dist/cjs/foundation/GlobalSearch.js +54 -0
  16. package/dist/cjs/foundation/GlobalSearch.js.map +1 -0
  17. package/dist/cjs/foundation/KanbanBoard.js +222 -0
  18. package/dist/cjs/foundation/KanbanBoard.js.map +1 -0
  19. package/dist/cjs/foundation/NotificationsButton.js +45 -0
  20. package/dist/cjs/foundation/NotificationsButton.js.map +1 -0
  21. package/dist/cjs/foundation/SubNavigation.js +72 -0
  22. package/dist/cjs/foundation/SubNavigation.js.map +1 -0
  23. package/dist/cjs/foundation/TabSet.js +101 -0
  24. package/dist/cjs/foundation/TabSet.js.map +1 -0
  25. package/dist/cjs/foundation/WizardProgress.js +185 -0
  26. package/dist/cjs/foundation/WizardProgress.js.map +1 -0
  27. package/dist/cjs/foundation/boardModel.js +67 -0
  28. package/dist/cjs/foundation/boardModel.js.map +1 -0
  29. package/dist/cjs/foundation/formModel.js +158 -0
  30. package/dist/cjs/foundation/formModel.js.map +1 -0
  31. package/dist/cjs/foundation/foundationComponentMetadata.js +54 -0
  32. package/dist/cjs/foundation/foundationComponentMetadata.js.map +1 -1
  33. package/dist/cjs/foundation/foundationDescriptors.js +400 -0
  34. package/dist/cjs/foundation/foundationDescriptors.js.map +1 -1
  35. package/dist/cjs/foundation/foundationSamples.js +303 -0
  36. package/dist/cjs/foundation/foundationSamples.js.map +1 -1
  37. package/dist/cjs/foundation/foundationStrings.js +72 -0
  38. package/dist/cjs/foundation/foundationStrings.js.map +1 -1
  39. package/dist/cjs/foundation/listModel.js +200 -0
  40. package/dist/cjs/foundation/listModel.js.map +1 -0
  41. package/dist/cjs/foundation/tabModel.js +41 -0
  42. package/dist/cjs/foundation/tabModel.js.map +1 -0
  43. package/dist/cjs/foundation/urlState.js +57 -0
  44. package/dist/cjs/foundation/urlState.js.map +1 -0
  45. package/dist/cjs/foundation/useCompactWidth.js +23 -0
  46. package/dist/cjs/foundation/useCompactWidth.js.map +1 -0
  47. package/dist/cjs/foundation/wizardModel.js +50 -0
  48. package/dist/cjs/foundation/wizardModel.js.map +1 -0
  49. package/dist/cjs/index.js +71 -0
  50. package/dist/cjs/index.js.map +1 -1
  51. package/dist/esm/ComponentName.d.ts +10 -1
  52. package/dist/esm/ComponentName.d.ts.map +1 -1
  53. package/dist/esm/ComponentName.js +9 -0
  54. package/dist/esm/ComponentName.js.map +1 -1
  55. package/dist/esm/defaultBlueprintComponents.d.ts.map +1 -1
  56. package/dist/esm/defaultBlueprintComponents.js +19 -1
  57. package/dist/esm/defaultBlueprintComponents.js.map +1 -1
  58. package/dist/esm/foundation/BulkBar.d.ts +4 -0
  59. package/dist/esm/foundation/BulkBar.d.ts.map +1 -0
  60. package/dist/esm/foundation/BulkBar.js +59 -0
  61. package/dist/esm/foundation/BulkBar.js.map +1 -0
  62. package/dist/esm/foundation/DataList.d.ts +3 -0
  63. package/dist/esm/foundation/DataList.d.ts.map +1 -0
  64. package/dist/esm/foundation/DataList.js +434 -0
  65. package/dist/esm/foundation/DataList.js.map +1 -0
  66. package/dist/esm/foundation/FormScreenBody.d.ts +3 -0
  67. package/dist/esm/foundation/FormScreenBody.d.ts.map +1 -0
  68. package/dist/esm/foundation/FormScreenBody.js +445 -0
  69. package/dist/esm/foundation/FormScreenBody.js.map +1 -0
  70. package/dist/esm/foundation/FoundationStates.js +2 -1
  71. package/dist/esm/foundation/FoundationStates.js.map +1 -1
  72. package/dist/esm/foundation/GlobalSearch.d.ts +3 -0
  73. package/dist/esm/foundation/GlobalSearch.d.ts.map +1 -0
  74. package/dist/esm/foundation/GlobalSearch.js +52 -0
  75. package/dist/esm/foundation/GlobalSearch.js.map +1 -0
  76. package/dist/esm/foundation/KanbanBoard.d.ts +3 -0
  77. package/dist/esm/foundation/KanbanBoard.d.ts.map +1 -0
  78. package/dist/esm/foundation/KanbanBoard.js +220 -0
  79. package/dist/esm/foundation/KanbanBoard.js.map +1 -0
  80. package/dist/esm/foundation/NotificationsButton.d.ts +3 -0
  81. package/dist/esm/foundation/NotificationsButton.d.ts.map +1 -0
  82. package/dist/esm/foundation/NotificationsButton.js +43 -0
  83. package/dist/esm/foundation/NotificationsButton.js.map +1 -0
  84. package/dist/esm/foundation/SubNavigation.d.ts +3 -0
  85. package/dist/esm/foundation/SubNavigation.d.ts.map +1 -0
  86. package/dist/esm/foundation/SubNavigation.js +70 -0
  87. package/dist/esm/foundation/SubNavigation.js.map +1 -0
  88. package/dist/esm/foundation/TabSet.d.ts +3 -0
  89. package/dist/esm/foundation/TabSet.d.ts.map +1 -0
  90. package/dist/esm/foundation/TabSet.js +99 -0
  91. package/dist/esm/foundation/TabSet.js.map +1 -0
  92. package/dist/esm/foundation/WizardProgress.d.ts +3 -0
  93. package/dist/esm/foundation/WizardProgress.d.ts.map +1 -0
  94. package/dist/esm/foundation/WizardProgress.js +183 -0
  95. package/dist/esm/foundation/WizardProgress.js.map +1 -0
  96. package/dist/esm/foundation/boardModel.d.ts +22 -0
  97. package/dist/esm/foundation/boardModel.d.ts.map +1 -0
  98. package/dist/esm/foundation/boardModel.js +61 -0
  99. package/dist/esm/foundation/boardModel.js.map +1 -0
  100. package/dist/esm/foundation/formModel.d.ts +48 -0
  101. package/dist/esm/foundation/formModel.d.ts.map +1 -0
  102. package/dist/esm/foundation/formModel.js +147 -0
  103. package/dist/esm/foundation/formModel.js.map +1 -0
  104. package/dist/esm/foundation/foundationComponentMetadata.d.ts.map +1 -1
  105. package/dist/esm/foundation/foundationComponentMetadata.js +54 -0
  106. package/dist/esm/foundation/foundationComponentMetadata.js.map +1 -1
  107. package/dist/esm/foundation/foundationDescriptors.d.ts.map +1 -1
  108. package/dist/esm/foundation/foundationDescriptors.js +400 -0
  109. package/dist/esm/foundation/foundationDescriptors.js.map +1 -1
  110. package/dist/esm/foundation/foundationSamples.d.ts.map +1 -1
  111. package/dist/esm/foundation/foundationSamples.js +303 -0
  112. package/dist/esm/foundation/foundationSamples.js.map +1 -1
  113. package/dist/esm/foundation/foundationStrings.d.ts +72 -0
  114. package/dist/esm/foundation/foundationStrings.d.ts.map +1 -1
  115. package/dist/esm/foundation/foundationStrings.js +72 -0
  116. package/dist/esm/foundation/foundationStrings.js.map +1 -1
  117. package/dist/esm/foundation/index.d.ts +16 -0
  118. package/dist/esm/foundation/index.d.ts.map +1 -1
  119. package/dist/esm/foundation/index.js +16 -0
  120. package/dist/esm/foundation/index.js.map +1 -1
  121. package/dist/esm/foundation/listModel.d.ts +49 -0
  122. package/dist/esm/foundation/listModel.d.ts.map +1 -0
  123. package/dist/esm/foundation/listModel.js +183 -0
  124. package/dist/esm/foundation/listModel.js.map +1 -0
  125. package/dist/esm/foundation/tabModel.d.ts +10 -0
  126. package/dist/esm/foundation/tabModel.d.ts.map +1 -0
  127. package/dist/esm/foundation/tabModel.js +37 -0
  128. package/dist/esm/foundation/tabModel.js.map +1 -0
  129. package/dist/esm/foundation/urlState.d.ts +4 -0
  130. package/dist/esm/foundation/urlState.d.ts.map +1 -0
  131. package/dist/esm/foundation/urlState.js +53 -0
  132. package/dist/esm/foundation/urlState.js.map +1 -0
  133. package/dist/esm/foundation/useCompactWidth.d.ts +3 -0
  134. package/dist/esm/foundation/useCompactWidth.d.ts.map +1 -0
  135. package/dist/esm/foundation/useCompactWidth.js +20 -0
  136. package/dist/esm/foundation/useCompactWidth.js.map +1 -0
  137. package/dist/esm/foundation/wizardModel.d.ts +16 -0
  138. package/dist/esm/foundation/wizardModel.d.ts.map +1 -0
  139. package/dist/esm/foundation/wizardModel.js +43 -0
  140. package/dist/esm/foundation/wizardModel.js.map +1 -0
  141. package/dist/esm/index.js +16 -0
  142. package/dist/esm/index.js.map +1 -1
  143. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  144. package/foundation/BulkBar.tsx +38 -0
  145. package/foundation/DataList.tsx +208 -0
  146. package/foundation/FormScreenBody.tsx +196 -0
  147. package/foundation/FoundationStates.tsx +1 -1
  148. package/foundation/GlobalSearch.tsx +33 -0
  149. package/foundation/KanbanBoard.tsx +97 -0
  150. package/foundation/NotificationsButton.tsx +28 -0
  151. package/foundation/SubNavigation.tsx +49 -0
  152. package/foundation/TabSet.tsx +65 -0
  153. package/foundation/WizardProgress.tsx +109 -0
  154. package/foundation/boardModel.ts +64 -0
  155. package/foundation/formModel.ts +146 -0
  156. package/foundation/foundationComponentMetadata.ts +9 -0
  157. package/foundation/foundationDescriptors.ts +195 -0
  158. package/foundation/foundationSamples.ts +56 -0
  159. package/foundation/foundationStrings.ts +72 -0
  160. package/foundation/index.ts +16 -0
  161. package/foundation/listModel.ts +190 -0
  162. package/foundation/tabModel.ts +41 -0
  163. package/foundation/urlState.ts +52 -0
  164. package/foundation/useCompactWidth.ts +23 -0
  165. package/foundation/wizardModel.ts +54 -0
  166. package/package.json +8 -8
  167. package/shell/layout.css +513 -0
@@ -7,6 +7,8 @@ import { ComponentName } from '../ComponentName';
7
7
  import { defaultBlueprintName } from '../packageName';
8
8
  import { ActivityKind } from './activityModel';
9
9
  import { ApprovalState } from './approvalModel';
10
+ import { CellKind, ListVariant } from './listModel';
11
+ import { FormFieldType, FormMode } from './formModel';
10
12
  import { ChartKind } from './chartGeometry';
11
13
  import { ComponentState } from './ComponentState';
12
14
  import { FilterFieldType } from './filterModel';
@@ -353,5 +355,198 @@ export const foundationDescriptors: ComponentDescriptor[] = [
353
355
  ...standardStateProperties,
354
356
  ],
355
357
  },
358
+ {
359
+ component: key(ComponentName.DataList),
360
+ displayName: 'Data list',
361
+ description: 'The records of a collection as a table, as cards or grouped with subtotals, with search, sort, paging and selection that live in the address. On a phone a table becomes cards.',
362
+ properties: [
363
+ { path: 'rows', label: 'Rows', group: 'Data', valueType: PropertyValueType.Json, acceptedBindingKinds: bindable, default: [], description: 'The records; bind to a query result.' },
364
+ {
365
+ ...list('columns', 'Columns', 'Data', 'Column', [
366
+ text('field', 'Field', 'Data', 'The result field the column shows.', { constraints: { required: true }, editorKind: PropertyEditorKind.Field }),
367
+ text('header', 'Header', 'Content'),
368
+ choice('kind', 'Kind', 'Appearance', { [CellKind.Text]: 'Text', [CellKind.Number]: 'Number', [CellKind.Currency]: 'Currency', [CellKind.Date]: 'Date', [CellKind.Status]: 'Status' }, CellKind.Text),
369
+ flag('sortable', 'Sortable', 'Behavior', undefined, { default: true }),
370
+ text('currency', 'Currency', 'Appearance'),
371
+ { path: 'tones', label: 'Status tones', group: 'Appearance', valueType: PropertyValueType.Object, editorKind: PropertyEditorKind.StringMap, description: 'The tone (success, warning, danger, info, neutral) of each status value.' },
372
+ ], 'What is shown.'),
373
+ acceptedBindingKinds: bindable,
374
+ },
375
+ choice('variant', 'Layout', 'Appearance', { [ListVariant.Table]: 'Table', [ListVariant.Cards]: 'Cards', [ListVariant.Grouped]: 'Grouped' }, ListVariant.Table),
376
+ flag('collapse', 'Cards on a phone', 'Appearance', 'A table becomes cards on a phone-wide screen; nothing is hidden.', { default: true }),
377
+ text('idField', 'Id field', 'Data', undefined, { default: 'id', editorKind: PropertyEditorKind.Field }),
378
+ text('titleField', 'Title field', 'Data', 'The field that names a row for assistive technology and a card.', { editorKind: PropertyEditorKind.Field }),
379
+ text('groupBy', 'Group by', 'Data', undefined, { editorKind: PropertyEditorKind.Field }),
380
+ text('totalField', 'Subtotal field', 'Data', undefined, { editorKind: PropertyEditorKind.Field }),
381
+ number('pageSize', 'Rows per page', 'Behavior', '0 shows every row.', { default: 0, constraints: { minimum: 0, integer: true } }),
382
+ choice('selectionMode', 'Selection', 'Behavior', { none: 'None', single: 'One row', multiple: 'Many rows' }, 'none'),
383
+ { path: 'rowActions', label: 'Row actions', group: 'Behavior', valueType: PropertyValueType.Json, editorKind: PropertyEditorKind.StringList, default: [], description: 'The actions in each row\'s menu; each is the action output.' },
384
+ rule('actionPermission', 'Row action permission', 'Whether row actions work, refuse with a reason or are not shown.'),
385
+ text('search', 'Search', 'Data', 'The search text; bind to the search box output.', { acceptedBindingKinds: bindable }),
386
+ { path: 'filter', label: 'Filter', group: 'Data', valueType: PropertyValueType.Object, acceptedBindingKinds: bindable, description: 'The filter builder\'s value.' },
387
+ text('sort', 'Sort', 'Data', 'The sorted field; a leading - is descending.'),
388
+ text('urlKey', 'Address prefix', 'Behavior', 'Keeps search, sort, page and selection in the address under this prefix.'),
389
+ valueOutputProperty(PropertyValueType.Json, 'The selected row id (single) or ids (many).'),
390
+ { path: 'action', label: 'Requested action', group: 'Outputs', valueType: PropertyValueType.Json, readOnly: true, output: true, description: 'The row action the user chose, with its row id.' },
391
+ ...standardStateProperties.filter(property => property.path !== 'actionPermission'),
392
+ ],
393
+ },
394
+ {
395
+ component: key(ComponentName.TabSet),
396
+ displayName: 'Tab set',
397
+ description: 'Tabs with a panel for each: the content of tab `overview` is the slot `overview`. The active tab is kept in the address.',
398
+ properties: [
399
+ {
400
+ ...list('tabs', 'Tabs', 'Data', 'Tab', [
401
+ text('id', 'Id', 'Data', 'Also the name of the slot that holds the tab\'s content.', { constraints: { required: true } }),
402
+ text('label', 'Label', 'Content'),
403
+ text('badge', 'Count', 'Content'),
404
+ flag('disabled', 'Disabled', 'Behavior'),
405
+ ], 'The tabs a record page offers.'),
406
+ acceptedBindingKinds: bindable,
407
+ },
408
+ text('activeTab', 'Active tab', 'Behavior'),
409
+ text('urlKey', 'Address parameter', 'Behavior', 'Keeps the active tab in the address under this name.'),
410
+ text('label', 'Label', 'Accessibility'),
411
+ valueOutputProperty(PropertyValueType.String, 'The id of the active tab.'),
412
+ ...standardStateProperties.filter(property => property.path !== 'label'),
413
+ ],
414
+ },
415
+ {
416
+ component: key(ComponentName.SubNavigation),
417
+ displayName: 'Sub-navigation',
418
+ description: 'A module\'s own sections as a tab bar or a left rail. Choosing one changes only the section.',
419
+ properties: [
420
+ {
421
+ ...list('items', 'Sections', 'Data', 'Section', [
422
+ text('id', 'Id', 'Data', undefined, { constraints: { required: true } }),
423
+ text('label', 'Label', 'Content'),
424
+ text('badge', 'Count', 'Content'),
425
+ flag('disabled', 'Disabled', 'Behavior'),
426
+ ], 'The sections.'),
427
+ acceptedBindingKinds: bindable,
428
+ },
429
+ choice('orientation', 'Orientation', 'Appearance', { horizontal: 'Tabs', vertical: 'Left rail' }, 'horizontal'),
430
+ text('activeItem', 'Active section', 'Behavior'),
431
+ text('urlKey', 'Address parameter', 'Behavior'),
432
+ text('label', 'Label', 'Accessibility'),
433
+ valueOutputProperty(PropertyValueType.String, 'The id of the active section.'),
434
+ ...standardStateProperties.filter(property => property.path !== 'label'),
435
+ ],
436
+ },
437
+ {
438
+ component: key(ComponentName.BulkBar),
439
+ displayName: 'Bulk bar',
440
+ description: 'Appears when records are selected, says how many, offers the bulk actions and clears the selection.',
441
+ properties: [
442
+ { path: 'selection', label: 'Selection', group: 'Data', valueType: PropertyValueType.Json, acceptedBindingKinds: bindable, description: 'The selected records or their count; bind to the list\'s value.' },
443
+ { path: 'actions', label: 'Actions', group: 'Behavior', valueType: PropertyValueType.Json, editorKind: PropertyEditorKind.StringList, default: [] },
444
+ rule('actionPermission', 'Action permission', 'Whether the bulk actions work, refuse with a reason or are not shown.'),
445
+ text('locale', 'Locale', 'Content'),
446
+ { path: 'strings', label: 'Text overrides', group: 'Content', valueType: PropertyValueType.Object, editorKind: PropertyEditorKind.StringMap },
447
+ ],
448
+ },
449
+ {
450
+ component: key(ComponentName.GlobalSearch),
451
+ displayName: 'Search',
452
+ description: 'A search box for the topbar or a list\'s toolbar. The text is the value output and can live in the address.',
453
+ properties: [
454
+ text('text', 'Initial text', 'Data'),
455
+ text('placeholder', 'Placeholder', 'Content'),
456
+ text('label', 'Label', 'Accessibility'),
457
+ text('urlKey', 'Address parameter', 'Behavior'),
458
+ valueOutputProperty(PropertyValueType.String, 'The search text.'),
459
+ { path: 'strings', label: 'Text overrides', group: 'Content', valueType: PropertyValueType.Object, editorKind: PropertyEditorKind.StringMap },
460
+ ],
461
+ },
462
+ {
463
+ component: key(ComponentName.NotificationsButton),
464
+ displayName: 'Notifications button',
465
+ description: 'The bell of the topbar with the unread count, said in words as well as in the badge.',
466
+ properties: [
467
+ number('count', 'Unread', 'Data', 'Bind to a query result.', { constraints: { minimum: 0, integer: true } }),
468
+ text('locale', 'Locale', 'Content'),
469
+ { path: 'strings', label: 'Text overrides', group: 'Content', valueType: PropertyValueType.Object, editorKind: PropertyEditorKind.StringMap },
470
+ ],
471
+ },
472
+ {
473
+ component: key(ComponentName.KanbanBoard),
474
+ displayName: 'Kanban board',
475
+ description: 'Columns mapped to a status, with cards that move by dragging or by their own Move-to menu. The new arrangement is the value output.',
476
+ properties: [
477
+ {
478
+ ...list('columns', 'Columns', 'Data', 'Column', [
479
+ text('id', 'Id', 'Data', 'The status or stage value.', { constraints: { required: true } }),
480
+ text('label', 'Label', 'Content'),
481
+ number('limit', 'Limit', 'Behavior', 'The most cards the stage should hold; 0 is no limit.', { constraints: { minimum: 0, integer: true } }),
482
+ ], 'The stages.'),
483
+ acceptedBindingKinds: bindable,
484
+ },
485
+ {
486
+ ...list('cards', 'Cards', 'Data', 'Card', [
487
+ text('id', 'Id', 'Data', undefined, { constraints: { required: true } }),
488
+ text('title', 'Title', 'Data', undefined, { constraints: { required: true } }),
489
+ text('column', 'Column', 'Data', 'The id of the column the card is in.'),
490
+ text('assignee', 'Assignee', 'Data'),
491
+ { path: 'badges', label: 'Badges', group: 'Data', valueType: PropertyValueType.Json, editorKind: PropertyEditorKind.StringList, default: [] },
492
+ ], 'The cards, from a query.'),
493
+ acceptedBindingKinds: bindable,
494
+ },
495
+ rule('editPermission', 'Move permission', 'Enabled lets cards move; otherwise the board is read-only.'),
496
+ valueOutputProperty(PropertyValueType.Json, 'Each card\'s id and column, published as cards move.'),
497
+ ...standardStateProperties,
498
+ ],
499
+ },
500
+ {
501
+ component: key(ComponentName.WizardProgress),
502
+ displayName: 'Wizard',
503
+ description: 'A linear wizard: the steps, back/next/finish and the draft save. The content of step `details` is the slot `details`; the last step is the review.',
504
+ properties: [
505
+ {
506
+ ...list('steps', 'Steps', 'Data', 'Step', [
507
+ text('id', 'Id', 'Data', 'Also the name of the slot that holds the step\'s content.', { constraints: { required: true } }),
508
+ text('label', 'Label', 'Content'),
509
+ flag('valid', 'Valid', 'Data', 'Whether the step\'s fields are valid; an invalid step blocks Next.', { default: true, acceptedBindingKinds: bindable }),
510
+ ], 'The steps in order.'),
511
+ acceptedBindingKinds: bindable,
512
+ },
513
+ text('currentStep', 'Current step', 'Behavior'),
514
+ text('urlKey', 'Address parameter', 'Behavior', 'Keeps the current step in the address so a refresh resumes.'),
515
+ valueOutputProperty(PropertyValueType.String, 'The id of the current step.'),
516
+ ...standardStateProperties,
517
+ ],
518
+ },
519
+ {
520
+ component: key(ComponentName.FormScreenBody),
521
+ displayName: 'Form',
522
+ description: 'Titled sections of fields in a one-to-three column grid, with inline validation, a save bar, the unsaved-changes guard and four modes.',
523
+ properties: [
524
+ {
525
+ ...list('sections', 'Sections', 'Data', 'Section', [
526
+ text('id', 'Id', 'Data'),
527
+ text('title', 'Title', 'Content'),
528
+ number('columns', 'Columns', 'Appearance', undefined, { default: 1, constraints: { minimum: 1, maximum: 3, integer: true } }),
529
+ list('fields', 'Fields', 'Data', 'Field', [
530
+ text('name', 'Name', 'Data', undefined, { constraints: { required: true }, editorKind: PropertyEditorKind.Field }),
531
+ text('label', 'Label', 'Content'),
532
+ choice('type', 'Type', 'Data', Object.fromEntries(Object.values(FormFieldType).map(type => [type, type])), FormFieldType.Text),
533
+ flag('required', 'Required', 'Data'),
534
+ { path: 'options', label: 'Options', group: 'Data', valueType: PropertyValueType.Json, editorKind: PropertyEditorKind.StringList, default: [] },
535
+ number('span', 'Columns spanned', 'Appearance', undefined, { default: 1, constraints: { minimum: 1, maximum: 3, integer: true } }),
536
+ text('placeholder', 'Placeholder', 'Content'),
537
+ text('help', 'Help text', 'Content'),
538
+ ]),
539
+ ], 'The groups of fields.'),
540
+ acceptedBindingKinds: bindable,
541
+ },
542
+ { path: 'values', label: 'Values', group: 'Data', valueType: PropertyValueType.Object, acceptedBindingKinds: bindable, description: 'The record being edited; empty when creating.' },
543
+ { path: 'errors', label: 'Server errors', group: 'Data', valueType: PropertyValueType.Json, acceptedBindingKinds: bindable, description: 'Field errors from the server, as { field: message }; one for an unknown field is shown as a general error.' },
544
+ choice('mode', 'Mode', 'Behavior', { [FormMode.Create]: 'Create', [FormMode.Edit]: 'Edit', [FormMode.ReadOnly]: 'Read-only', [FormMode.Settings]: 'Settings (a save for each section)' }, FormMode.Create),
545
+ rule('editPermission', 'Edit permission', 'Enabled lets the user change fields; otherwise the form is read-only.'),
546
+ rule('savePermission', 'Save permission', 'Whether Save works, refuses with a reason or is not shown.'),
547
+ valueOutputProperty(PropertyValueType.Object, 'The values as the user has edited them.'),
548
+ ...standardStateProperties,
549
+ ],
550
+ },
356
551
  ];
357
552
 
@@ -108,6 +108,62 @@ export const foundationSamples: Record<string, FoundationSample> = {
108
108
  },
109
109
  data: ['Edit deals', 'Manage users'], stateful: true,
110
110
  },
111
+ [ComponentName.DataList]: {
112
+ properties: {
113
+ locale: 'en-US', idField: 'id', titleField: 'name', selectionMode: 'multiple', pageSize: 5, rowActions: ['Open', 'Archive'],
114
+ columns: [
115
+ { field: 'name', header: 'Account' }, { field: 'owner', header: 'Owner' }, { field: 'value', header: 'Value', kind: 'currency', currency: 'USD' },
116
+ { field: 'status', header: 'Status', kind: 'status', tones: { Open: 'info', Won: 'success', Lost: 'danger' } }, { field: 'closes', header: 'Closes', kind: 'date' },
117
+ ],
118
+ rows: [
119
+ { id: '1', name: 'Northgate Ltd', owner: 'Ada', value: 12000, status: 'Open', closes: '2026-04-01' },
120
+ { id: '2', name: 'Harbour & Sons', owner: 'Grace', value: 8400.5, status: 'Won', closes: '2026-03-12' },
121
+ { id: '3', name: 'Fjord Supply', owner: 'Linus', value: 560, status: 'Lost', closes: '2026-02-20' },
122
+ ],
123
+ },
124
+ data: ['Northgate Ltd', '3 items'], stateful: true,
125
+ },
126
+ [ComponentName.TabSet]: {
127
+ properties: { tabs: [{ id: 'overview', label: 'Overview' }, { id: 'related', label: 'Related', badge: '4' }, { id: 'files', label: 'Files' }, { id: 'history', label: 'History', disabled: true }] },
128
+ data: ['Overview', 'Related'], stateful: true,
129
+ },
130
+ [ComponentName.SubNavigation]: {
131
+ properties: { items: [{ id: 'accounts', label: 'Accounts' }, { id: 'contacts', label: 'Contacts', badge: '12' }, { id: 'deals', label: 'Deals' }] },
132
+ data: ['Accounts', 'Contacts'], stateful: true,
133
+ },
134
+ [ComponentName.BulkBar]: { properties: { selection: ['1', '2'], actions: ['Assign', 'Archive'] }, data: [], stateful: false },
135
+ [ComponentName.GlobalSearch]: { properties: { placeholder: 'Search accounts', label: 'Search accounts' }, data: [], stateful: false },
136
+ [ComponentName.NotificationsButton]: { properties: { count: 3 }, data: [], stateful: false },
137
+ [ComponentName.KanbanBoard]: {
138
+ properties: {
139
+ columns: [{ id: 'new', label: 'New', limit: 2 }, { id: 'qualified', label: 'Qualified' }, { id: 'won', label: 'Won' }],
140
+ cards: [
141
+ { id: 'c1', title: 'Acme renewal', column: 'new', assignee: 'Ada', fields: [{ label: 'Value', value: '$12,000' }], badges: ['Hot'] },
142
+ { id: 'c2', title: 'Globex rollout', column: 'new', assignee: 'Grace' },
143
+ { id: 'c3', title: 'Initech pilot', column: 'new' },
144
+ { id: 'c4', title: 'Umbrella upgrade', column: 'qualified' },
145
+ ],
146
+ },
147
+ data: ['Acme renewal'], stateful: true,
148
+ },
149
+ [ComponentName.WizardProgress]: {
150
+ properties: { steps: [{ id: 'details', label: 'Details' }, { id: 'people', label: 'People' }, { id: 'review', label: 'Review' }] },
151
+ data: ['Details', 'People'], stateful: true,
152
+ },
153
+ [ComponentName.FormScreenBody]: {
154
+ properties: {
155
+ mode: 'create', locale: 'en-US',
156
+ sections: [
157
+ { title: 'Account details', columns: 2, fields: [
158
+ { name: 'name', label: 'Name', required: true }, { name: 'industry', label: 'Industry', type: 'select', options: ['Software', 'Retail'] },
159
+ { name: 'revenue', label: 'Revenue', type: 'currency' }, { name: 'since', label: 'Customer since', type: 'date' },
160
+ { name: 'notes', label: 'Notes', type: 'richText', span: 2 },
161
+ ] },
162
+ { title: 'Preferences', fields: [{ name: 'tags', label: 'Tags', type: 'multiSelect', options: ['VIP', 'Partner'] }, { name: 'active', label: 'Active', type: 'toggle' }] },
163
+ ],
164
+ },
165
+ data: ['Account details'], stateful: true,
166
+ },
111
167
  };
112
168
 
113
169
  /** A scene element for a foundation component, populated with its sample unless properties are given, with the state forced when asked. */
@@ -124,6 +124,78 @@ export const foundationStrings = {
124
124
  orgReports: '{count} reports',
125
125
  orgTree: 'Organization',
126
126
  orgNone: 'There is nobody in the organization yet',
127
+ listCount: '{count} items',
128
+ listCountFiltered: '{shown} of {total} items',
129
+ listSelectAll: 'Select all rows on this page',
130
+ listSelectRow: 'Select {name}',
131
+ listRowActions: 'Actions for {name}',
132
+ listActions: 'Actions',
133
+ listSortBy: 'Sort by {column}',
134
+ listSortedAscending: 'sorted ascending',
135
+ listSortedDescending: 'sorted descending',
136
+ listPage: 'Page {page} of {pages}',
137
+ listPrevious: 'Previous page',
138
+ listNext: 'Next page',
139
+ listNoMatches: 'Nothing matches your search or filters',
140
+ listClearSearch: 'Clear search and filters',
141
+ listGroupCollapse: 'Collapse {group}',
142
+ listGroupExpand: 'Expand {group}',
143
+ listGroupSubtotal: '{count} items, {total}',
144
+ listCards: 'Items as cards',
145
+ listTable: 'Items as a table',
146
+ listBack: 'Back to the list',
147
+ viewTable: 'Table',
148
+ viewCards: 'Cards',
149
+ viewGrouped: 'Grouped',
150
+ densityComfortable: 'Comfortable',
151
+ densityCompact: 'Compact',
152
+ formRequired: 'required',
153
+ formRequiredField: '{label} is required',
154
+ formInvalidField: '{label} is not valid',
155
+ formSummary: 'Fix {count} fields before saving',
156
+ formSummaryOne: 'Fix 1 field before saving',
157
+ formSave: 'Save',
158
+ formSaveAndNew: 'Save and new',
159
+ formCancel: 'Cancel',
160
+ formEdit: 'Edit',
161
+ formUnsaved: 'You have unsaved changes',
162
+ formDiscard: 'Discard changes',
163
+ formSaved: 'Saved',
164
+ formSectionSave: 'Save {section}',
165
+ formReadOnly: 'This form is read-only',
166
+ formChoose: 'Choose…',
167
+ summaryLabel: 'Key facts',
168
+ tabsLabel: 'Sections',
169
+ subNavLabel: 'Module sections',
170
+ bulkSelected: '{count} selected',
171
+ bulkClear: 'Clear selection',
172
+ bulkActions: 'Actions for the selection',
173
+ searchLabel: 'Search',
174
+ searchPlaceholder: 'Search',
175
+ searchClear: 'Clear search',
176
+ notificationsLabel: 'Notifications',
177
+ notificationsCount: '{count} unread',
178
+ notificationsNone: 'No new notifications',
179
+ boardMoveTo: 'Move to…',
180
+ boardMoveCard: 'Move {title}',
181
+ boardMoved: '{title} moved to {column}',
182
+ boardColumn: '{column}, {count} cards',
183
+ boardCollapse: 'Collapse {column}',
184
+ boardExpand: 'Expand {column}',
185
+ boardWip: 'Limit {limit}',
186
+ boardWipExceeded: 'Over the limit of {limit}',
187
+ boardNoCards: 'No cards',
188
+ wizardStep: 'Step {n} of {total}',
189
+ wizardBack: 'Back',
190
+ wizardNext: 'Next',
191
+ wizardFinish: 'Finish',
192
+ wizardSaveDraft: 'Save draft',
193
+ wizardDraftSaved: 'Draft saved',
194
+ wizardReview: 'Review',
195
+ wizardComplete: 'Completed',
196
+ wizardCurrent: 'Current step',
197
+ wizardUpcoming: 'Not started',
198
+ wizardProgress: 'Wizard progress',
127
199
  matrixAllowed: 'Allowed',
128
200
  matrixDenied: 'Not allowed',
129
201
  matrixGrant: 'Grant {permission} to {role}',
@@ -36,6 +36,22 @@ export * from './orgModel';
36
36
  export * from './OrgTree';
37
37
  export * from './permissionModel';
38
38
  export * from './PermissionMatrix';
39
+ export * from './urlState';
40
+ export * from './useCompactWidth';
41
+ export * from './listModel';
42
+ export * from './DataList';
43
+ export * from './tabModel';
44
+ export * from './TabSet';
45
+ export * from './SubNavigation';
46
+ export * from './BulkBar';
47
+ export * from './GlobalSearch';
48
+ export * from './NotificationsButton';
49
+ export * from './boardModel';
50
+ export * from './KanbanBoard';
51
+ export * from './wizardModel';
52
+ export * from './WizardProgress';
53
+ export * from './formModel';
54
+ export * from './FormScreenBody';
39
55
  export * from './foundationDescriptors';
40
56
  export * from './foundationComponentMetadata';
41
57
  export * from './foundationSamples';
@@ -0,0 +1,190 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** How a column's cell is drawn. */
5
+ export enum CellKind {
6
+ Text = 'text',
7
+ Number = 'number',
8
+ Currency = 'currency',
9
+ Date = 'date',
10
+ Status = 'status',
11
+ }
12
+
13
+ /** One column of a list. */
14
+ export interface ListColumn {
15
+ field: string;
16
+ header: string;
17
+ kind: CellKind;
18
+ sortable: boolean;
19
+ /** For a status column: the tone of each value (`success`, `warning`, `danger`, `info`, `neutral`). */
20
+ tones: Record<string, string>;
21
+ /** For a currency column: the ISO 4217 code. */
22
+ currency?: string;
23
+ }
24
+
25
+ /** One record of a list, with the field values as the query returned them. */
26
+ export type ListRow = Record<string, unknown>;
27
+
28
+ /** How the list is sorted. */
29
+ export interface ListSort {
30
+ field: string;
31
+ descending: boolean;
32
+ }
33
+
34
+ /** The ways the same records can be laid out. */
35
+ export enum ListVariant {
36
+ Table = 'table',
37
+ Cards = 'cards',
38
+ Grouped = 'grouped',
39
+ }
40
+
41
+ const records = (value: unknown): Record<string, unknown>[] =>
42
+ Array.isArray(value) ? value.filter((entry): entry is Record<string, unknown> => typeof entry === 'object' && entry !== null && !Array.isArray(entry)) : [];
43
+
44
+ /** Reads columns, dropping those without a field and repeated fields. */
45
+ export function readListColumns(value: unknown): ListColumn[] {
46
+ const seen = new Set<string>();
47
+ return records(value).flatMap(entry => {
48
+ if (typeof entry.field !== 'string' || entry.field.length === 0 || seen.has(entry.field)) return [];
49
+ seen.add(entry.field);
50
+ const tones = typeof entry.tones === 'object' && entry.tones !== null && !Array.isArray(entry.tones)
51
+ ? Object.fromEntries(Object.entries(entry.tones as Record<string, unknown>).filter((pair): pair is [string, string] => typeof pair[1] === 'string')) : {};
52
+ return [{
53
+ field: entry.field,
54
+ header: typeof entry.header === 'string' && entry.header.length > 0 ? entry.header : entry.field,
55
+ kind: (Object.values(CellKind) as string[]).includes(entry.kind as string) ? (entry.kind as CellKind) : CellKind.Text,
56
+ sortable: entry.sortable !== false,
57
+ tones,
58
+ ...(typeof entry.currency === 'string' ? { currency: entry.currency } : {}),
59
+ }];
60
+ });
61
+ }
62
+
63
+ /** Reads the variant, defaulting to the table. */
64
+ export const readListVariant = (value: unknown): ListVariant => ((Object.values(ListVariant) as string[]).includes(value as string) ? (value as ListVariant) : ListVariant.Table);
65
+
66
+ /** The value of a row's field as text for searching and display: a primitive as itself, anything else empty. */
67
+ export const textOf = (row: ListRow, field: string): string => {
68
+ const value = row[field];
69
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? String(value) : '';
70
+ };
71
+
72
+ /** The id of a row: its `idField`, else its position, so a list without ids still selects and keys sensibly. */
73
+ export const rowId = (row: ListRow, index: number, idField: string): string => {
74
+ const value = row[idField];
75
+ return typeof value === 'string' || typeof value === 'number' ? String(value) : `row-${index}`;
76
+ };
77
+
78
+ /** Rows that contain the search text in any shown column, without regard to case or accents; empty search keeps every row. */
79
+ export function searchRows(rows: ListRow[], columns: ListColumn[], search: string): ListRow[] {
80
+ const needle = normalize(search);
81
+ if (needle.length === 0) return rows;
82
+ return rows.filter(row => columns.some(column => normalize(textOf(row, column.field)).includes(needle)));
83
+ }
84
+
85
+ /** Rows that satisfy a filter of conditions joined by AND or OR, in nested groups - the value the filter builder publishes. */
86
+ export function filterRows(rows: ListRow[], filter: unknown): ListRow[] {
87
+ if (typeof filter !== 'object' || filter === null) return rows;
88
+ const group = filter as { operator?: string; conditions?: { field: string; operator: string; value: string }[]; groups?: unknown[] };
89
+ const tests = [
90
+ ...(group.conditions ?? []).filter(condition => typeof condition?.field === 'string').filter(active).map(condition => (row: ListRow) => satisfies(row, condition)),
91
+ ...(group.groups ?? []).filter(nested => hasConditions(nested)).map(nested => (row: ListRow) => filterRows([row], nested).length === 1),
92
+ ];
93
+ if (tests.length === 0) return rows;
94
+ return rows.filter(row => (group.operator === 'or' ? tests.some(test => test(row)) : tests.every(test => test(row))));
95
+ }
96
+
97
+ const active = (condition: { operator: string; value: string }) => ['isEmpty', 'true', 'false'].includes(condition.operator) || (condition.value ?? '').trim().length > 0;
98
+ const hasConditions = (group: unknown): boolean => {
99
+ const candidate = group as { conditions?: { operator: string; value: string }[]; groups?: unknown[] } | null;
100
+ return (candidate?.conditions ?? []).some(active) || (candidate?.groups ?? []).some(hasConditions);
101
+ };
102
+
103
+ function satisfies(row: ListRow, condition: { field: string; operator: string; value: string }): boolean {
104
+ const raw = row[condition.field];
105
+ const text = normalize(typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean' ? String(raw) : '');
106
+ const value = normalize(condition.value ?? '');
107
+ const number = Number(raw);
108
+ const wanted = Number(condition.value);
109
+ switch (condition.operator) {
110
+ case 'contains': return text.includes(value);
111
+ case 'equals': case 'is': case 'on': return typeof raw === 'number' ? number === wanted : text === value;
112
+ case 'notEquals': case 'isNot': return typeof raw === 'number' ? number !== wanted : text !== value;
113
+ case 'startsWith': return text.startsWith(value);
114
+ case 'isEmpty': return text.length === 0;
115
+ case 'greater': return number > wanted;
116
+ case 'greaterOrEqual': return number >= wanted;
117
+ case 'less': return number < wanted;
118
+ case 'lessOrEqual': return number <= wanted;
119
+ case 'before': return text < value;
120
+ case 'after': return text > value;
121
+ case 'true': return raw === true || text === 'true';
122
+ case 'false': return raw === false || text === 'false';
123
+ default: return true;
124
+ }
125
+ }
126
+
127
+ function normalize(text: string): string {
128
+ return text.normalize('NFD').replace(/\p{Diacritic}/gu, '').toLocaleLowerCase();
129
+ }
130
+
131
+ /** Reads a sort from its address form, `field` or `-field` for descending; a field that is not a sortable column is no sort at all. */
132
+ export function parseSort(text: string | undefined, columns: ListColumn[]): ListSort | undefined {
133
+ if (!text) return undefined;
134
+ const descending = text.startsWith('-');
135
+ const field = descending ? text.slice(1) : text;
136
+ return columns.some(column => column.field === field && column.sortable) ? { field, descending } : undefined;
137
+ }
138
+
139
+ /** A sort in its address form. */
140
+ export const formatSort = (sort: ListSort | undefined): string | undefined => (sort === undefined ? undefined : `${sort.descending ? '-' : ''}${sort.field}`);
141
+
142
+ /** The next sort when a header is pressed: ascending, then descending, then none. */
143
+ export const nextSort = (current: ListSort | undefined, field: string): ListSort | undefined =>
144
+ current?.field !== field ? { field, descending: false } : current.descending ? undefined : { field, descending: true };
145
+
146
+ /** Rows in order of a column, numbers as numbers and text by the locale, with ties kept in their original order. */
147
+ export function sortRows(rows: ListRow[], sort: ListSort | undefined, locale: string): ListRow[] {
148
+ if (!sort) return rows;
149
+ const collator = new Intl.Collator(locale, { numeric: true, sensitivity: 'base' });
150
+ return rows.map((row, index) => ({ row, index })).sort((left, right) => {
151
+ const a = left.row[sort.field];
152
+ const b = right.row[sort.field];
153
+ const order = typeof a === 'number' && typeof b === 'number' ? a - b : collator.compare(textOf(left.row, sort.field), textOf(right.row, sort.field));
154
+ return (order === 0 ? left.index - right.index : sort.descending ? -order : order);
155
+ }).map(entry => entry.row);
156
+ }
157
+
158
+ /** The slice of rows on a page (1-based), and how many pages there are; a page out of range is clamped rather than empty. */
159
+ export function paginate<T>(rows: T[], page: number, pageSize: number): { rows: T[]; page: number; pages: number } {
160
+ const size = pageSize > 0 ? Math.floor(pageSize) : rows.length || 1;
161
+ const pages = Math.max(1, Math.ceil(rows.length / size));
162
+ const clamped = Math.min(Math.max(1, Math.floor(Number.isFinite(page) ? page : 1)), pages);
163
+ return { rows: rows.slice((clamped - 1) * size, clamped * size), page: clamped, pages };
164
+ }
165
+
166
+ /** A group of rows with a subtotal, for the grouped variant. */
167
+ export interface ListGroup {
168
+ key: string;
169
+ rows: ListRow[];
170
+ subtotal?: number;
171
+ }
172
+
173
+ /** Groups rows by a field in the order the groups are first seen, with the sum of a numeric field as the subtotal when one is named. */
174
+ export function groupRows(rows: ListRow[], groupBy: string, totalField?: string): ListGroup[] {
175
+ const groups: ListGroup[] = [];
176
+ for (const row of rows) {
177
+ const key = textOf(row, groupBy);
178
+ const group = groups.find(candidate => candidate.key === key) ?? (groups.push({ key, rows: [] }), groups[groups.length - 1]);
179
+ group.rows.push(row);
180
+ }
181
+
182
+ return totalField === undefined ? groups : groups.map(group => ({ ...group, subtotal: group.rows.reduce((sum, row) => sum + (typeof row[totalField] === 'number' ? (row[totalField] as number) : 0), 0) }));
183
+ }
184
+
185
+ /** Toggles an id in a selection that allows one or many. */
186
+ export const toggleSelection = (selected: string[], id: string, multiple: boolean): string[] =>
187
+ selected.includes(id) ? selected.filter(candidate => candidate !== id) : multiple ? [...selected, id] : [id];
188
+
189
+ /** Reads a selection from its address form: ids separated by commas. */
190
+ export const parseSelection = (text: string | undefined): string[] => (text ? text.split(',').filter(id => id.length > 0) : []);
@@ -0,0 +1,41 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** One tab or sub-section: an id the address and the slot content use, a label, and an optional count badge. */
5
+ export interface TabItem {
6
+ id: string;
7
+ label: string;
8
+ badge?: string;
9
+ disabled: boolean;
10
+ }
11
+
12
+ /** Reads tabs out of a property value; entries without an id are dropped and a repeated id keeps its first. */
13
+ export function readTabs(value: unknown): TabItem[] {
14
+ if (!Array.isArray(value)) return [];
15
+
16
+ const seen = new Set<string>();
17
+ return value.flatMap((entry): TabItem[] => {
18
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return [];
19
+ const { id, label, badge, disabled } = entry as Record<string, unknown>;
20
+ if (typeof id !== 'string' || id.length === 0 || seen.has(id)) return [];
21
+ seen.add(id);
22
+ return [{ id, label: typeof label === 'string' && label.length > 0 ? label : id, disabled: disabled === true, ...(typeof badge === 'string' || typeof badge === 'number' ? { badge: String(badge) } : {}) }];
23
+ });
24
+ }
25
+
26
+ /** The active tab: the requested one when it exists and is enabled, else the first enabled one. Deep links to a tab that was removed fall back instead of showing nothing. */
27
+ export function activeTab(tabs: TabItem[], requested: string | undefined): string | undefined {
28
+ const wanted = tabs.find(tab => tab.id === requested && !tab.disabled);
29
+ return (wanted ?? tabs.find(tab => !tab.disabled))?.id;
30
+ }
31
+
32
+ /** The enabled tab one step from the current one, wrapping, or the first/last for Home and End. */
33
+ export function stepTab(tabs: TabItem[], current: string | undefined, direction: 'next' | 'previous' | 'first' | 'last'): string | undefined {
34
+ const enabled = tabs.filter(tab => !tab.disabled);
35
+ if (enabled.length === 0) return undefined;
36
+ if (direction === 'first') return enabled[0].id;
37
+ if (direction === 'last') return enabled[enabled.length - 1].id;
38
+
39
+ const index = Math.max(0, enabled.findIndex(tab => tab.id === current));
40
+ return enabled[(index + (direction === 'next' ? 1 : -1) + enabled.length) % enabled.length].id;
41
+ }