@orangelogic/design-system 3.17.0 → 3.19.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 (139) hide show
  1. package/library/chunks/{asset-link-format.FmSSysED.js → asset-link-format.ttcRQgDc.js} +6 -6
  2. package/library/chunks/{button.DCyoZrRj.js → button.D1YIxtv-.js} +1 -1
  3. package/library/chunks/{color-swatch-group.CvApE9bf.js → color-swatch-group.Bumfw-G2.js} +4788 -4335
  4. package/library/chunks/{color-swatch.DxxOpJXT.js → color-swatch.DdsUysbF.js} +1 -1
  5. package/library/chunks/{confirm-popover.-_1n24v-.js → confirm-popover.CJXyEa5A.js} +3 -3
  6. package/library/chunks/{dialog.Dl9aHLD4.js → dialog.BbQ4Du0M.js} +2 -2
  7. package/library/chunks/{document-viewer.DY5s9zRW.js → document-viewer.C9tM2Gc7.js} +7 -7
  8. package/library/chunks/{dot-pagination.BNoG-qx7.js → dot-pagination.CJRAtM51.js} +1 -1
  9. package/library/chunks/{folder-select.CsuADJH9.js → folder-select.BAPsI-gg.js} +4 -4
  10. package/library/chunks/gradient.CIzq8fOU.js +176 -0
  11. package/library/chunks/{header.v8maWlzT.js → header.BioJzPlg.js} +1 -1
  12. package/library/chunks/{i18n.CNnI4-0K.js → i18n.rtxrs1c1.js} +11 -3
  13. package/library/chunks/{icon-button.5-oAlIkv.js → icon-button.CbUddcgI.js} +1 -1
  14. package/library/chunks/{iframe.DOwG9Q7o.js → iframe.y7iCAVZP.js} +1 -1
  15. package/library/chunks/{image.BSh7WAGF.js → image.A2hE1WoV.js} +1 -1
  16. package/library/chunks/{index.CW7LXCxo.js → index.Coy2OPB4.js} +14 -14
  17. package/library/chunks/{input.Csf-S2nB.js → input.D1FFprPl.js} +1 -1
  18. package/library/chunks/{markdown.b03AOvFT.js → markdown.Dd_GFJK8.js} +2 -2
  19. package/library/chunks/{menu-item.HtTWOcq4.js → menu-item.0TbnDEDp.js} +1 -1
  20. package/library/chunks/{option.DgxoCq1j.js → option.CyorNG0S.js} +17 -17
  21. package/library/chunks/{pagination.C9NblxKt.js → pagination.D0BeaEIi.js} +5 -5
  22. package/library/chunks/{tab-group.B6Aej-jM.js → tab-group.C98CWzEH.js} +4 -4
  23. package/library/chunks/{toast.CGIDPKV8.js → toast.BxiewnAG.js} +1 -1
  24. package/library/chunks/{transformation.OaRYixzE.js → transformation.CjWnFhab.js} +12 -11
  25. package/library/chunks/{tree.DKofDMGu.js → tree.B7IKBX3n.js} +7 -4
  26. package/library/chunks/{types.BNc8NEZr.js → types.DVZ_cbKn.js} +14220 -13897
  27. package/library/components/alert.js +2 -2
  28. package/library/components/asset-link-format.js +4 -4
  29. package/library/components/atoms.js +15 -15
  30. package/library/components/audio.js +3 -3
  31. package/library/components/border-input-group.js +2 -2
  32. package/library/components/breadcrumb.js +1 -1
  33. package/library/components/button.js +2 -2
  34. package/library/components/color-picker.js +971 -337
  35. package/library/components/color-swatch-group.js +5 -5
  36. package/library/components/color-swatch.js +2 -2
  37. package/library/components/confirm-popover.js +4 -4
  38. package/library/components/copy-button.js +1 -1
  39. package/library/components/corner-position-input-group.js +3 -3
  40. package/library/components/date-calendar.js +1 -1
  41. package/library/components/details.js +1 -1
  42. package/library/components/dialog.js +3 -3
  43. package/library/components/document-viewer.js +3 -3
  44. package/library/components/dot-pagination.js +2 -2
  45. package/library/components/drawer.js +2 -2
  46. package/library/components/dropdown.js +2 -2
  47. package/library/components/dynamic-select.js +4 -4
  48. package/library/components/e-chart.js +1 -1
  49. package/library/components/element-clamp.js +2 -2
  50. package/library/components/file-on-demand.js +9 -9
  51. package/library/components/folder-select.js +4 -4
  52. package/library/components/format-bytes.js +1 -1
  53. package/library/components/format-date.js +1 -1
  54. package/library/components/format-number.js +1 -1
  55. package/library/components/format-time.js +1 -1
  56. package/library/components/header.js +2 -2
  57. package/library/components/icon-button.js +2 -2
  58. package/library/components/iframe.js +2 -2
  59. package/library/components/image-comparer.js +1 -1
  60. package/library/components/image.js +2 -2
  61. package/library/components/input.js +2 -2
  62. package/library/components/line-clamp.js +1 -1
  63. package/library/components/markdown.js +3 -3
  64. package/library/components/masonry.js +2 -2
  65. package/library/components/menu-item.js +2 -2
  66. package/library/components/menu.js +2 -2
  67. package/library/components/molecules.js +3 -3
  68. package/library/components/mutation-observer.js +1 -1
  69. package/library/components/option.js +2 -2
  70. package/library/components/organisms.js +2 -2
  71. package/library/components/padding-input-group.js +1 -1
  72. package/library/components/pagination.js +6 -6
  73. package/library/components/popup.js +1 -1
  74. package/library/components/position-picker.js +1 -1
  75. package/library/components/progress-bar.js +1 -1
  76. package/library/components/progress-ring.js +1 -1
  77. package/library/components/range.js +1 -1
  78. package/library/components/rating.js +1 -1
  79. package/library/components/relative-time.js +1 -1
  80. package/library/components/select.js +2 -2
  81. package/library/components/shadow-input-group.js +1 -1
  82. package/library/components/share-option-list.js +1 -1
  83. package/library/components/sidebar.js +1 -1
  84. package/library/components/size-input-group.js +3 -3
  85. package/library/components/spinner.js +1 -1
  86. package/library/components/split-panel.js +1 -1
  87. package/library/components/tab-group.js +5 -5
  88. package/library/components/tab.js +2 -2
  89. package/library/components/tag.js +2 -2
  90. package/library/components/tooltip.js +1 -1
  91. package/library/components/tree-item.js +2 -2
  92. package/library/components/tree.js +2 -2
  93. package/library/components/typeface.js +2 -2
  94. package/library/components/types.js +199 -188
  95. package/library/components/video.js +3 -3
  96. package/library/package.json +1 -1
  97. package/library/packages/atoms/src/components/audio/audio.d.ts +1 -0
  98. package/library/packages/atoms/src/components/color-picker/color-picker.d.ts +160 -2
  99. package/library/packages/atoms/src/components/color-picker/components/gradient-slider/gradient-slider.d.ts +80 -0
  100. package/library/packages/atoms/src/components/color-picker/components/gradient-slider/gradient-slider.styles.d.ts +2 -0
  101. package/library/packages/atoms/src/components/color-picker/components/gradient-stop-list/gradient-stop-list.d.ts +85 -0
  102. package/library/packages/atoms/src/components/color-picker/components/gradient-stop-list/gradient-stop-list.styles.d.ts +2 -0
  103. package/library/packages/atoms/src/components/input/input.d.ts +1 -1
  104. package/library/packages/atoms/src/components/option/option.d.ts +1 -1
  105. package/library/packages/atoms/src/components/tree/tree.d.ts +2 -2
  106. package/library/packages/atoms/src/components/video/video.d.ts +1 -0
  107. package/library/packages/events/src/cx-gradient-change.d.ts +10 -0
  108. package/library/packages/events/src/cx-gradient-stop-add.d.ts +9 -0
  109. package/library/packages/events/src/cx-gradient-stop-change.d.ts +10 -0
  110. package/library/packages/events/src/cx-gradient-stop-remove.d.ts +8 -0
  111. package/library/packages/events/src/cx-gradient-stop-select.d.ts +8 -0
  112. package/library/packages/events/src/cx-keydown.d.ts +4 -1
  113. package/library/packages/events/src/events.d.ts +5 -0
  114. package/library/packages/molecules/src/multi-select/components/board/board.d.ts +58 -2
  115. package/library/packages/molecules/src/multi-select/components/board-list-item/board-list-item.d.ts +14 -0
  116. package/library/packages/molecules/src/multi-select/controllers/filter-controller.d.ts +68 -0
  117. package/library/packages/molecules/src/multi-select/multi-select.d.ts +211 -0
  118. package/library/packages/molecules/src/multi-select/utils/base.d.ts +44 -0
  119. package/library/packages/molecules/src/multi-select/utils/reconcile.d.ts +57 -0
  120. package/library/packages/organisms/src/bento-grid/bento-grid.d.ts +32 -11
  121. package/library/packages/organisms/src/carousel/carousel.d.ts +1 -0
  122. package/library/packages/organisms/src/content-builder/blocks/audio/audio.d.ts +1 -0
  123. package/library/packages/organisms/src/content-builder/blocks/image/image.d.ts +27 -4
  124. package/library/packages/organisms/src/content-builder/content-builder-constants.d.ts +12 -0
  125. package/library/packages/organisms/src/content-builder/content-builder.d.ts +150 -0
  126. package/library/packages/organisms/src/content-builder/content-builder.utils.d.ts +20 -0
  127. package/library/packages/organisms/src/content-builder/plugins/wrapper-container.d.ts +11 -0
  128. package/library/packages/organisms/src/content-builder-viewer/content-builder-viewer.d.ts +10 -2
  129. package/library/packages/types/src/board.d.ts +25 -0
  130. package/library/packages/types/src/color-picker.d.ts +35 -2
  131. package/library/packages/utils/src/custom-element/i18n.d.ts +8 -0
  132. package/library/packages/utils/src/gradient/gradient.d.ts +65 -0
  133. package/library/packages/utils/src/gradient/index.d.ts +1 -0
  134. package/library/packages/utils/src/index.d.ts +1 -0
  135. package/library/packages/utils/src/transformation/transformation.d.ts +2 -0
  136. package/library/react-web-component.d.ts +497 -469
  137. package/library/utils.js +162 -151
  138. package/package.json +1 -1
  139. package/library/packages/molecules/src/multi-select/multi-select.utils.d.ts +0 -3
@@ -1,21 +1,34 @@
1
+ import { default as CxDivider } from '../../../../../atoms/src/components/divider/divider.ts';
1
2
  import { default as CxIcon } from '../../../../../atoms/src/components/icon/icon.ts';
2
3
  import { default as CxIconButton } from '../../../../../atoms/src/components/icon-button/icon-button.ts';
3
4
  import { default as CxInput } from '../../../../../atoms/src/components/input/input.ts';
4
5
  import { default as CxTooltip } from '../../../../../atoms/src/components/tooltip/tooltip.ts';
5
6
  import { default as CortexElement } from '../../../../../base/src/cortex-element.ts';
6
- import { BoardItem } from '../../../../../types/src/board';
7
+ import { BoardItem, HiddenSelectionState } from '../../../../../types/src/board';
7
8
  import { default as Sortable } from 'sortablejs';
8
9
  import { default as CxBoardListItem } from '../board-list-item/board-list-item';
9
10
 
10
11
  /**
11
12
  * @summary Board is a list of items that can be reordered by dragging and dropping them.
13
+ * @description One column of `cx-multi-select`. Owns its own selection, an optional search input,
14
+ * and a virtualised window over the rows. What it displays is narrowed by its own search term;
15
+ * the host narrows it further by handing it narrowed `data`. On drag and drop the board reports
16
+ * back exactly the rows it holds.
12
17
  *
13
- * @event {{ items: string[] }} cx-sort-change - Emitted when the list items changes.
18
+ * @event {{ id: string; isMultiDrag?: boolean; items: string[]; newIndex?: number; type: string }} cx-board-change - Emitted when rows are added, removed, or reordered. `items` is the board's full id list after the change; `type` is SortableJS's `add` / `remove` / `update`.
19
+ * @event {{ id: string; items: string[]; type?: string }} cx-board-selected-change - Emitted when the row selection changes. `items` is the selected ids in the order they were picked.
20
+ *
21
+ * @csspart divider - The rule under the header. Only rendered when `divider-color` is set.
22
+ * @csspart list - The scrolling row container.
23
+ * @csspart search-input - The search input. Only rendered when `allow-search` is set.
24
+ * @csspart search-input-clear - The clear button inside the search input.
25
+ * @csspart select-all - The select-all checkbox in the header. Only rendered when `all` is set.
14
26
  */
15
27
  export default class CxBoard extends CortexElement {
16
28
  static readonly styles: import('lit').CSSResult[];
17
29
  static readonly dependencies: {
18
30
  'cx-board-list-item': typeof CxBoardListItem;
31
+ 'cx-divider': typeof CxDivider;
19
32
  'cx-icon': typeof CxIcon;
20
33
  'cx-icon-button': typeof CxIconButton;
21
34
  'cx-input': typeof CxInput;
@@ -36,6 +49,28 @@ export default class CxBoard extends CortexElement {
36
49
  configurable: boolean;
37
50
  all: boolean;
38
51
  allowSearch: boolean;
52
+ /**
53
+ * Color of the rule under the board header. Empty (the default) draws no rule at all — not a
54
+ * transparent one — so a board that says nothing about it keeps byte-for-byte the header it has
55
+ * always had, rather than every existing consumer silently gaining a 1px line.
56
+ *
57
+ * One property rather than a separate show/hide flag: the color's presence *is* the switch, so
58
+ * there is no "shown but uncolored" or "colored but hidden" state to reconcile.
59
+ *
60
+ * Takes any CSS color, but prefer a design token — `var(--cx-color-primary-500)` — over a raw
61
+ * hex so the rule follows the theme.
62
+ */
63
+ dividerColor: string;
64
+ /**
65
+ * Display-only: the host's summary of selected rows this board cannot see (the host may be
66
+ * narrowing `data`). Read in exactly four render outputs — the header glyph, its
67
+ * `aria-checked`, the tint, and the footer count — and never acted on: it does not touch
68
+ * `data`, the selection, drag, or any event. Same species of input as `divider-color`, not a
69
+ * control channel.
70
+ */
71
+ hiddenSelection: HiddenSelectionState;
72
+ /** Display-only: how many selected rows the host is hiding; added to the footer count. */
73
+ hiddenSelectedCount: number;
39
74
  /**
40
75
  * A boolean property that indicates whether the drag operation should be aborted.
41
76
  * When set to `true`, the current drag action will be canceled,
@@ -66,7 +101,21 @@ export default class CxBoard extends CortexElement {
66
101
  private previous;
67
102
  private lastSelectedItem;
68
103
  get selectedCount(): number;
104
+ /**
105
+ * Whether the tick, rather than the dash, is what the header should show.
106
+ *
107
+ * With rows on screen the board answers from what it sees: every visible selectable row
108
+ * selected. With nothing left on screen there is no row to describe, so it defers to
109
+ * `hiddenSelection` — the host's display-only summary of the selection this board cannot
110
+ * see. `All` there means every selectable row of the whole column is selected, which is
111
+ * exactly what the tick claims.
112
+ */
69
113
  get isAllSelected(): boolean;
114
+ /**
115
+ * Something is selected that the tick cannot stand for — including a selection the host is
116
+ * currently hiding, which is then the only thing still telling the user a selection
117
+ * survives the filter.
118
+ */
70
119
  get isPartiallySelected(): boolean;
71
120
  /**
72
121
  * Rows counted toward "select all" (excludes synthetic group headers, disabled, and
@@ -118,6 +167,13 @@ export default class CxBoard extends CortexElement {
118
167
  handleMove(evt: Sortable.MoveEvent): boolean | undefined;
119
168
  deselectAll(force?: boolean): void;
120
169
  selectAll(): void;
170
+ /**
171
+ * Restores a selection after the host replaced `data` (which resets it), without emitting
172
+ * `cx-board-selected-change` — only the selection's owner calls this, so it already knows.
173
+ * Ids the board does not currently hold are skipped; the order of `ids` becomes the
174
+ * selection order.
175
+ */
176
+ restoreSelection(ids: string[]): void;
121
177
  handleSelectAll(): void;
122
178
  private handleSelectAllKeydown;
123
179
  /** Index in `this.items` immediately after the last real (non-header) visible row. */
@@ -5,6 +5,20 @@ import { default as CxTooltip } from '../../../../../atoms/src/components/toolti
5
5
  import { default as CortexElement } from '../../../../../base/src/cortex-element.ts';
6
6
  import { TemplateResult } from 'lit';
7
7
 
8
+ /**
9
+ * @summary A single row inside `cx-board`: a selection indicator, the row's label, and an
10
+ * optional configure button.
11
+ * @description The label is clamped to two lines; hovering it opens the row's `tooltip` content,
12
+ * which is rendered as HTML. The configure button appears only when the row is `configurable` and
13
+ * not `readonly`, and stops its own click from reaching the board's row-selection handler.
14
+ *
15
+ * @event {{ id: string }} cx-multi-select-configure - Emitted when the configure button is activated. Carries the row's `data-id`.
16
+ *
17
+ * @csspart config-button - The configure (cog) button.
18
+ * @csspart text - The wrapper around the row's label.
19
+ * @csspart text-content - The clamped label itself.
20
+ * @csspart tooltip - The tooltip holding the row's `tooltip` content.
21
+ */
8
22
  export default class CxBoardListItem extends CortexElement {
9
23
  static readonly styles: import('lit').CSSResult[];
10
24
  static readonly dependencies: {
@@ -0,0 +1,68 @@
1
+ import { BoardItem } from '../../../../types/src/board';
2
+ import { SelectedFacets } from '../../../../types/src/faceted-filter';
3
+
4
+ /**
5
+ * Where a filter came from.
6
+ *
7
+ * The `facet:` prefix is load-bearing rather than cosmetic: `facetCountingPool` tells facet
8
+ * sources apart from every other source by it, and that distinction *is* the drill-down rule.
9
+ * One source per facet field, mirroring how `cx-table`'s `FilterController` keys its own
10
+ * `dynamic:facet:<field>` sources.
11
+ */
12
+ export type MultiSelectFilterSource = 'search' | `facet:${string}`;
13
+ /**
14
+ * The single owner of every filter `cx-multi-select` applies to its two boards.
15
+ *
16
+ * Each filter registers itself as a named source holding one predicate, and nothing outside this
17
+ * class decides how sources combine. Adding a filter is therefore one `set…` call in the handler
18
+ * that owns it: the AND below and the facet-counting pool both pick it up with no further edit.
19
+ * That is the failure this replaces — search used to narrow the boards without narrowing the pool
20
+ * the facet counts were tallied over, so the panel advertised counts no row could reach.
21
+ *
22
+ * Modelled on `packages/hybrid/table-core/src/table/controllers/filter-controller.ts`. That one
23
+ * hands filter descriptors to Tabulator, which does the filtering; there is no engine on this
24
+ * side, so sources hold predicates this class evaluates itself.
25
+ */
26
+ export declare class MultiSelectFilterController {
27
+ private readonly sources;
28
+ get hasAnySource(): boolean;
29
+ /**
30
+ * Registers the toolbar search term, or drops the source when it is empty.
31
+ *
32
+ * Case-insensitive `includes` with no trimming — deliberately the same test `cx-board` applies
33
+ * to its own header search, so the two boxes select the same rows for the same text. They
34
+ * differ only in when they run: this one on Enter, the board's on every keystroke.
35
+ */
36
+ setSearch(searchValue: string): void;
37
+ /**
38
+ * Reconciles one source per facet field from the whole selection map, the way
39
+ * `FilterController.syncFacetedFilters` does: fields that lost their last value are dropped,
40
+ * fields that have values are re-registered. Taking the entire map rather than one field at a
41
+ * time is what makes clearing a value as ordinary as adding one — there is no separate removal
42
+ * path to forget.
43
+ *
44
+ * AND across fields, since each field is its own source; OR within a field, via the set below.
45
+ */
46
+ syncFacetSources(selectedFacets: SelectedFacets): void;
47
+ /**
48
+ * Rows passing every source, or `null` when nothing is filtering.
49
+ *
50
+ * `null` rather than "every row" lets the caller hand the board its full list untouched —
51
+ * same array, no filtering cost; an empty array means the opposite: no row may show.
52
+ * Result identity across calls is the caller's business (`stabilizeItems`), not this
53
+ * class's — two boards share this controller with two different pools, so a single cache
54
+ * here would thrash.
55
+ */
56
+ filterItems(pool: BoardItem[]): BoardItem[] | null;
57
+ /**
58
+ * The pool a facet's values are counted over: every source applied *except* the facet ones.
59
+ *
60
+ * This is the whole drill-down policy, in one place. Leaving facet sources out is what keeps a
61
+ * value the user has not ticked yet reachable — counted against its siblings rather than
62
+ * against itself. The remaining facet-versus-facet part (a facet skips only its own constraint,
63
+ * never another facet's) belongs to `buildMetadataCounts`, which resolves it for every facet in
64
+ * a single pass.
65
+ */
66
+ facetCountingPool(pool: BoardItem[]): BoardItem[];
67
+ private matchesEverySource;
68
+ }
@@ -1,6 +1,9 @@
1
+ import { default as CxIcon } from '../../../atoms/src/components/icon/icon.ts';
1
2
  import { default as CxIconButton } from '../../../atoms/src/components/icon-button/icon-button.ts';
3
+ import { default as CxInput } from '../../../atoms/src/components/input/input.ts';
2
4
  import { default as CxTooltip } from '../../../atoms/src/components/tooltip/tooltip.ts';
3
5
  import { default as CortexElement } from '../../../base/src/cortex-element.ts';
6
+ import { default as CxFacetedFilter } from '../faceted-filter/faceted-filter.ts';
4
7
  import { ColumnData } from '../../../types/src/multi-select';
5
8
  import { PropertyValues, TemplateResult } from 'lit';
6
9
  import { default as CxBoard } from './components/board/board';
@@ -9,17 +12,45 @@ import { default as CxBoard } from './components/board/board';
9
12
  * @summary Multi-select allows the user to select multiple items from a list. The items can be moved between two columns. The user can move items between the columns by dragging and dropping them.
10
13
  * @element cx-multi-select
11
14
  *
15
+ * A toolbar above the two boards holds the filters. `canSearch` adds a search box there, applied
16
+ * on Enter, and giving items a `metadata` object adds a facet panel built from it, counted across
17
+ * both columns — `labelMap` then renames, reorders, or restricts those facets.
18
+ * Both narrow what the two boards display without changing the data they hold, so a selection
19
+ * made before filtering survives and still moves across with the arrow buttons — still-visible
20
+ * rows stay ticked, hidden ones are summarized on each board's header and footer.
21
+ *
22
+ * The filter panel opens whenever either is configured, since it also holds "Applied filters":
23
+ * the search term shows there as a removable chip, and the panel's "Clear" wipes the search along
24
+ * with any facets. With no `metadata` the panel simply has no facets to tick.
25
+ *
26
+ * Separately, `canSearchBoard` controls the search input each board renders in its own header.
27
+ * It defaults to the first board, matching how this component has always looked; set it to `''`
28
+ * and turn on `canSearch` to move that search up into the toolbar instead.
29
+ *
12
30
  * @event {{ id: string; items: BoardItem[] }} cx-multi-select-change - Emitted when the list items change.
31
+ * @event {{ id: string }} cx-multi-select-configure - Emitted when the cog on a second-column row is activated. Requires `configurable`.
32
+ *
33
+ * @csspart board-1 - The first ("available") board.
34
+ * @csspart board-2 - The second ("selected") board.
35
+ * @csspart faceted-filter - The filter panel above the boards, holding the metadata facets and the applied-filter chips. Rendered whenever the toolbar is.
36
+ * @csspart search-input - The search box in the toolbar. Only rendered when `can-search` is set.
37
+ * @csspart search-input-clear - The clear button inside the search box, forwarded from `cx-input`'s
38
+ * own `clear-button` part. Only present while the box has text.
39
+ * @csspart tooltip - The tooltips wrapping the two move buttons.
40
+ *
41
+ * @cssproperty [--search-width=227px] - Width of the search box in the toolbar.
13
42
  *
14
43
  * @typedef BoardItem - {
15
44
  * group: string;
16
45
  * id: string;
17
46
  * index?: string;
47
+ * metadata?: Record<string, string>;
18
48
  * text: string;
19
49
  * tooltip: string;
20
50
  * }
21
51
  *
22
52
  * @typedef ColumnData - {
53
+ * dividerColor?: string;
23
54
  * id: string;
24
55
  * items: BoardItem[];
25
56
  * sort?: boolean;
@@ -30,10 +61,19 @@ export default class CxMultiSelect extends CortexElement {
30
61
  static readonly styles: import('lit').CSSResult[];
31
62
  static readonly dependencies: {
32
63
  'cx-board': typeof CxBoard;
64
+ 'cx-faceted-filter': typeof CxFacetedFilter;
65
+ 'cx-icon': typeof CxIcon;
33
66
  'cx-icon-button': typeof CxIconButton;
67
+ 'cx-input': typeof CxInput;
34
68
  'cx-tooltip': typeof CxTooltip;
35
69
  };
36
70
  private readonly localize;
71
+ /**
72
+ * The single owner of the toolbar's filters. Both of them — the search box and the metadata
73
+ * facets — register themselves here as named sources, and it is this object, not any one
74
+ * handler, that decides how they combine. See `./filter-controller`.
75
+ */
76
+ private readonly filters;
37
77
  multiSelect: HTMLElement;
38
78
  board1: CxBoard;
39
79
  board2: CxBoard;
@@ -41,15 +81,100 @@ export default class CxMultiSelect extends CortexElement {
41
81
  secondColumnData: ColumnData;
42
82
  configurable: boolean;
43
83
  ignoreTypes: string[];
84
+ /**
85
+ * Shows a search box in the toolbar that narrows both boards by item text. It applies on Enter
86
+ * (or when cleared), not while typing — unlike the input `cx-board` renders in its own header,
87
+ * which filters on every keystroke.
88
+ *
89
+ * Independent of `labelMap`: search works with no facets configured, and facets work with
90
+ * search turned off.
91
+ */
92
+ canSearch: boolean;
93
+ /**
94
+ * Which boards render their own search input in their header, as a space-separated list of
95
+ * board positions — `'1'`, `'2'`, `'1 2'`, or `''` for neither. Space-separated to match how
96
+ * every other list-in-a-string attribute in the design system reads (`cx-dot-pagination`,
97
+ * `cx-menu`, `cx-popup`, …) and native HTML lists like `class` and `part`.
98
+ *
99
+ * Defaults to `'1'` because the first board has always shown that input; leaving it alone keeps
100
+ * every existing consumer looking exactly as it does today. Set it to `''` alongside
101
+ * `can-search` to move the search up into the toolbar, which is the point of having both:
102
+ * turning one on does not turn the other off, so a host that set both would show two search
103
+ * boxes filtering the same rows.
104
+ *
105
+ * A plain string rather than an array on purpose. The C# mirror serializes `string[]` as a JSON
106
+ * array while a delimiter-splitting Lit converter expects `'1 2'`, and the generator never sees
107
+ * the converter — the mismatch that leaves `cx-dot-pagination`'s `control-ids` unusable from the
108
+ * platform. A `string` goes down the same path as `href` or `label`, which does work.
109
+ */
110
+ canSearchBoard: string;
111
+ /**
112
+ * Maps a `BoardItem.metadata` key to the facet group name shown in the filter panel. Doubles as
113
+ * the allowlist and the display order: naming three keys out of ten is how a host says the
114
+ * other seven are not filterable.
115
+ *
116
+ * Leaving it empty (the default) does not hide the filter — it falls back to every metadata key
117
+ * present, each headed by the raw key. So the panel appears as soon as items carry `metadata`,
118
+ * and `labelMap` is only needed to rename facets, reorder them, or leave some out. A host that
119
+ * wants no filter at all sends no `metadata`.
120
+ */
121
+ labelMap: Record<string, string>;
44
122
  addLimit: number | undefined;
45
123
  private itemMap;
46
124
  selectedItems: Record<string, string[]>;
47
125
  private initialSecondColumnItems;
126
+ /** Facet config in `labelMap` order — bound to `cx-faceted-filter`'s `availableFacets`. */
127
+ private metadataFacetDetails;
128
+ /** Facet values with their drill-down counts — bound to `cx-faceted-filter`'s `facets`. */
129
+ private metadataFacets;
130
+ /** Values the user has ticked in the filter panel, keyed by metadata key. */
131
+ private selectedMetadataFacets;
132
+ /**
133
+ * The search term currently being applied — only what the user has committed with Enter or
134
+ * wiped with the clear button, never what is merely typed. The box itself holds the in-progress
135
+ * text, so there is no second field mirroring it here.
136
+ */
137
+ searchValue: string;
48
138
  private hasCapturedSecondColumnBaseline;
49
139
  private board1Data;
50
140
  private board2Data;
141
+ /**
142
+ * The original (unfiltered), sort-rule-applied list each board would show with no filter —
143
+ * NOT a snapshot of the consumer's initial data: it is re-derived in willUpdate() on every
144
+ * column-data change. Two jobs: the pool `recomputeBoardData` filters over, and a sort cache
145
+ * so the sort runs once per data change instead of on every filter recompute. With the
146
+ * default `sort` this is the very same reference as the column's `items`; only `sort: false`
147
+ * makes it a sorted copy.
148
+ */
149
+ private board1OriginalData;
150
+ private board2OriginalData;
51
151
  get firstColumnItemIds(): string[];
52
152
  get secondColumnItemIds(): string[];
153
+ /**
154
+ * The facet key -> heading map actually in force.
155
+ *
156
+ * `labelMap` wins when the host supplies one, because it is an allowlist as well as a set of
157
+ * labels: naming three keys out of ten is how a host says the other seven are not filterable.
158
+ * With no `labelMap` there is nothing to allowlist against, so every metadata key present
159
+ * becomes a facet headed by the raw key — attaching `metadata` is then enough to get a filter.
160
+ */
161
+ private get effectiveLabelMap();
162
+ /**
163
+ * Chips handed to `cx-faceted-filter` for filters it does not own itself — currently just the
164
+ * toolbar search term.
165
+ *
166
+ * This is what puts the search term under the panel's "Clear", and it is the only way to get it
167
+ * there: "Clear" reports itself by emitting `cx-faceted-filter-remove` once per chip supplied
168
+ * here, so with no chips it says nothing about the search at all. It also feeds the panel's
169
+ * applied-filter count, which is what decides whether the "Clear" button renders — without a
170
+ * chip, a search with no facet ticked leaves the count at zero and no button to press.
171
+ *
172
+ * Not gated on `canSearch`: if a term is somehow applied while the box is hidden it is still
173
+ * narrowing both boards, and a chip is then the only way to see and remove it.
174
+ */
175
+ private get appliedFilterTags();
176
+ /** Board positions parsed out of `canSearchBoard`, tolerant of extra whitespace. */
177
+ private get boardsWithOwnSearch();
53
178
  /**
54
179
  * Validates the changes made to the items array based on the specified type of action ('add' or 'remove').
55
180
  * If an add limit is set, it checks whether the new items exceed the allowed limit.
@@ -66,8 +191,94 @@ export default class CxMultiSelect extends CortexElement {
66
191
  private handleMoveItems;
67
192
  private handleMoveItemsRight;
68
193
  private handleMoveItemsLeft;
194
+ /**
195
+ * Commits what is in the box on Enter — nothing filters until then. There is no separate draft
196
+ * field because `cx-input` is already holding the typed text: this only has to read it back at
197
+ * the moment the user commits. The `value` binding cannot fight that, since Lit re-commits an
198
+ * attribute binding only when the bound value changes, and `searchValue` changes only here.
199
+ *
200
+ * The parameter is typed with the payload `cx-input` actually emits (`{ event, value }`)
201
+ * rather than `CxKeydownEvent`, whose declared detail is empty — using the alias would type
202
+ * every detail read as `never`. Same situation as `cx-rich-text-toolbar`'s stepper handler,
203
+ * minus its cast.
204
+ *
205
+ * `preventDefault()` does double duty: `cx-input` schedules a form submit on an unmodified
206
+ * Enter and only skips it when the event was default-prevented, so without this an Enter here
207
+ * would submit whatever form the component sits in. The `isComposing` guard mirrors the one
208
+ * `cx-input` applies to that same submit — with an IME open, Enter is picking a candidate, not
209
+ * asking to search.
210
+ */
211
+ private handleSearchKeydown;
212
+ /**
213
+ * Clears the applied term — the clear button is its own commit, so it does not wait for Enter.
214
+ * `cx-input` has already emptied itself and taken focus back by the time this runs.
215
+ */
216
+ private handleSearchClear;
217
+ /**
218
+ * Removes one of the chips fed in through `appliedFilterTags`, either from its own X or from
219
+ * the panel's "Clear", which emits this once per chip before clearing its own facets.
220
+ *
221
+ * Assigning `searchValue` is the whole fix: the `@watch` below drops the search source from the
222
+ * controller, and the input's `value` binding empties the box. Unknown keys are ignored rather
223
+ * than thrown on — the panel may carry chips from filters added later.
224
+ */
225
+ private handleAppliedFilterRemove;
226
+ private handleMetadataFacetChange;
227
+ /**
228
+ * Rebuilds the filter panel's own data — the facet config and the values with their counts.
229
+ *
230
+ * The pool is `itemMap` rather than the two `items` arrays concatenated: `itemMap` is keyed by
231
+ * id, so an item present in both columns collapses to one entry. Concatenating would count it
232
+ * twice for every item the deduplication above has not cut yet.
233
+ *
234
+ * `facetCountingPool` then applies every non-facet filter to it, so a count always predicts how
235
+ * many rows ticking that value would actually leave on screen. Which sources that covers is the
236
+ * controller's business, not this method's — that is the point of asking it rather than naming
237
+ * the search term here, as this once did and would have to keep doing for every filter added.
238
+ *
239
+ * A count can still reach zero for a value the user has already ticked; `buildMetadataCounts`
240
+ * keeps such values in the tally so they stay visible and removable.
241
+ */
242
+ private rebuildMetadataFacets;
243
+ /**
244
+ * Pushes the controller's verdict into what the two boards receive. Only this derived,
245
+ * per-board view is filtered — the full column state (and with it `itemMap`, drag
246
+ * reporting, and the facet counting pool) always keeps every row the host owns.
247
+ *
248
+ * `stabilizeItems` hands back the previous array when nothing changed, because `cx-board`
249
+ * treats every new `data` reference as a real data change and resets its selection and
250
+ * scroll. An unrelated re-render must not do that.
251
+ */
252
+ private recomputeBoardData;
253
+ /**
254
+ * Puts the visible part of the tracked selection back onto each board after a filter change.
255
+ *
256
+ * A filter change hands both boards a new `data` reference, and `cx-board` answers any data
257
+ * change by resetting its own selection — it cannot tell a filter from a real row change.
258
+ * This component can, so once the boards have processed the new data it restores what it
259
+ * still tracks. Ids the filter hides are not on the board to restore; they stay tracked
260
+ * here, which is what keeps the move buttons live and the display inputs truthful for them.
261
+ */
262
+ private restoreBoardSelections;
263
+ /**
264
+ * No debouncing here, and none wanted: `searchValue` moves only when the user commits with
265
+ * Enter or clears the box, so this runs once per deliberate action rather than once per
266
+ * keystroke. The input holds the text in the meantime.
267
+ */
268
+ onSearchValueChanged(): Promise<void>;
69
269
  onSecondColumnDataChange(): void;
70
270
  protected willUpdate(changedProperties: PropertyValues<this>): void;
271
+ /**
272
+ * The filter panel is rendered whenever the toolbar is, even with no facets to tick.
273
+ *
274
+ * Its facet list is only half of what it shows: the other half is "Applied filters", which holds
275
+ * the search chip and the "Clear" that removes it. Gating the whole panel on having facets took
276
+ * that away from anyone filtering by search alone — with no `metadata`, there was no panel, so
277
+ * no way to see or clear the term except the box's own X. `cx-faceted-filter` already renders
278
+ * its applied-filters header unconditionally and only its facet body on having facets, so an
279
+ * empty facet list costs nothing here.
280
+ */
281
+ private renderFilterToolbar;
71
282
  render(): TemplateResult;
72
283
  }
73
284
  declare global {
@@ -0,0 +1,44 @@
1
+ import { BoardItem } from '../../../../types/src/board';
2
+ import { SelectedFacets } from '../../../../types/src/faceted-filter';
3
+ import { MetadataFacetData } from '../../../../types/src/multi-select';
4
+
5
+ export declare const compareFunction: (a: BoardItem, b: BoardItem) => number;
6
+ /**
7
+ * Counts items per `(facet key, value)` pair. The counting unit is the pair, never the value on
8
+ * its own — the same string under two different keys is two unrelated facet values.
9
+ *
10
+ * Counts drill down: a facet is tallied over the items satisfying every *other* selected facet,
11
+ * never its own. That is what keeps the values a user has not picked yet reachable instead of
12
+ * dropping them to zero the moment a sibling value is ticked.
13
+ *
14
+ * One pass over `items` is enough for that, because a facet counts an item exactly when the item
15
+ * fails no constraint other than the facet's own. So the item's number of failures decides
16
+ * everything: none means every facet counts it, exactly one means only the facet owning that
17
+ * failing constraint counts it, and two or more means no facet can — excluding one still leaves
18
+ * the other unsatisfied.
19
+ *
20
+ * Only keys present in `labelMap` become facets, so the host can push any metadata it likes and
21
+ * still control what is filterable. Facets that end up with no values are dropped rather than
22
+ * rendered empty.
23
+ */
24
+ export declare function buildMetadataCounts(items: BoardItem[], labelMap: Record<string, string>, selectedFacets?: SelectedFacets): Map<string, Map<string, number>>;
25
+ /**
26
+ * Every metadata key present in `items`, mapped to itself — the fallback for a host that attaches
27
+ * metadata but does not say what to call each facet. Attaching metadata is then enough to get a
28
+ * filter, with the raw key as the group heading.
29
+ *
30
+ * Keys keep the order they are first seen in rather than being sorted, so the facet order follows
31
+ * the order the host wrote its own `metadata` objects in — the same thing `labelMap` controls when
32
+ * it is supplied. Re-assigning a key that is already present does not move it, so the plain
33
+ * assignment below is enough to keep that order.
34
+ */
35
+ export declare function deriveLabelMapFromMetadata(items: BoardItem[]): Record<string, string>;
36
+ /**
37
+ * Maps raw counts onto the pair of bindings `cx-faceted-filter` expects.
38
+ *
39
+ * Both arrays are built from the same `labelMap` key order and share one `facetDetails` object
40
+ * per facet, because the component renders by iterating `availableFacets` and then looking the
41
+ * facet up in `facets` by `facetFieldName` — a mismatch between the two drops that facet
42
+ * silently.
43
+ */
44
+ export declare function toMetadataFacetData(counts: Map<string, Map<string, number>>, labelMap: Record<string, string>): MetadataFacetData;
@@ -0,0 +1,57 @@
1
+ import { BoardItem, HiddenSelectionState } from '../../../../types/src/board';
2
+
3
+ /**
4
+ * Pure helpers that let `cx-multi-select` hand each `cx-board` a *filtered view* as plain
5
+ * `data` while its own column state keeps every row.
6
+ *
7
+ * The board is deliberately dumb: it only ever sees, and reports back, the rows it was given.
8
+ * With a filter active that report describes the visible list, not the column — so every
9
+ * board-reported change has to be merged back into the full list here, every board-relative
10
+ * index translated, and the selection the board cannot see summarized for its display-only
11
+ * inputs. With no filter active the visible list IS the full list and every function below
12
+ * degrades to the identity (or `None`), which keeps pre-filter behavior byte-identical.
13
+ */
14
+ /** What `computeHiddenSelection` reports for one column. */
15
+ export interface HiddenSelectionSummary {
16
+ count: number;
17
+ state: HiddenSelectionState;
18
+ }
19
+ /**
20
+ * Returns `previous` when `next` holds exactly the same item objects in the same order, so an
21
+ * unchanged result keeps its array identity. Lit compares reactive properties by reference,
22
+ * and `cx-board` answers every new `data` reference by resetting its selection and scroll
23
+ * position — identity is what stops an unrelated re-render from doing that.
24
+ */
25
+ export declare function stabilizeItems(next: BoardItem[], previous: BoardItem[]): BoardItem[];
26
+ /**
27
+ * Translates an insertion index expressed against the visible (filtered) list into an index in
28
+ * the full list, anchoring on the visible row the insertion lands before — hidden rows keep
29
+ * their position relative to their visible neighbors. Past the visible end, the insertion
30
+ * lands right after the last visible row (before any hidden tail), which is where the user
31
+ * saw it go; with nothing visible at all it appends at the absolute end.
32
+ */
33
+ export declare function toFullInsertIndex(visibleIndex: number, visibleItems: BoardItem[], fullItems: BoardItem[]): number;
34
+ /**
35
+ * Merges a board-reported visible id list back into the full list the host owns.
36
+ *
37
+ * The report is treated as a delta against the visible list the host handed the board:
38
+ * visible rows missing from the report leave the full list, new ids are kept where the report
39
+ * puts them, and every hidden row stays attached to (in front of) the visible row that
40
+ * followed it — so hidden rows travel with their anchor on reorder and are never lost. A
41
+ * hidden group whose anchor itself left the board joins the tail: its position is
42
+ * unobservable while hidden, and the tail is the one placement that cannot collide with
43
+ * whatever the user just arranged.
44
+ */
45
+ export declare function mergeVisibleListIntoFull(reportedIds: string[], visibleBefore: BoardItem[], fullItems: BoardItem[]): string[];
46
+ /**
47
+ * Describes the part of a column's tracked selection that the board cannot see, feeding the
48
+ * two display-only inputs `cx-board` renders (`hidden-selection`, `hidden-selected-count`).
49
+ *
50
+ * `All` mirrors what the header tick means when rows are visible — every selectable row of
51
+ * the whole column is selected — so "select all, then filter to nothing" keeps its tick and a
52
+ * partial selection keeps its dash. The board itself cannot make that call: it only holds the
53
+ * filtered rows, and the distinction needs the full column. The selectable exclusions mirror
54
+ * the board's own `selectableItems` (no synthetic group headers, no disabled rows, no ignored
55
+ * types).
56
+ */
57
+ export declare function computeHiddenSelection(trackedSelectedIds: string[], visibleItems: BoardItem[], fullItems: BoardItem[], ignoreTypes: string[]): HiddenSelectionSummary;
@@ -42,7 +42,7 @@ export default class CxBentoGrid extends CortexElement {
42
42
  /** Cached viewport height — updated on resize only, not on every scroll. */
43
43
  private _viewportHeight;
44
44
  /** Watches the host element for size changes so the grid reflows when its container resizes. */
45
- private _resizeObserver;
45
+ private resizeObserver;
46
46
  /** Tracks the last emitted range to avoid firing cx-bento-range-change when the range hasn't changed. */
47
47
  private _lastEmittedFirst;
48
48
  private _lastEmittedLast;
@@ -273,6 +273,11 @@ export default class CxBentoGrid extends CortexElement {
273
273
  runConnectedCallback(): void;
274
274
  disconnectedCallback(): void;
275
275
  runFirstUpdated(): void;
276
+ /**
277
+ * ResizeObserver on the container covers both window resize and any container-driven
278
+ * width changes (sidebar toggle, flex layout shifts, etc.).
279
+ */
280
+ private observeContainerResize;
276
281
  /**
277
282
  * Returns the visible viewport height for virtualization.
278
283
  *
@@ -339,8 +344,27 @@ export default class CxBentoGrid extends CortexElement {
339
344
  * instead of stopping it.
340
345
  */
341
346
  private scheduleRowRelease;
342
- /** Clears any pending release and resets the gate back to its initial state. */
347
+ /** Drops the pending release, keeping the rows already released. */
348
+ private cancelRowRelease;
349
+ /**
350
+ * Clears any pending release and resets the gate back to its initial state.
351
+ *
352
+ * Must run BEFORE `computeLayout()` in the same pass: that call ends by arming the
353
+ * next release, and a reset landing after it cancels the arm with nothing left to
354
+ * re-arm, which gates every row for good.
355
+ */
343
356
  private resetRowRelease;
357
+ /**
358
+ * Restarts row gating when the dataset shrinks or is replaced wholesale.
359
+ *
360
+ * Row indices refer to a specific layout, so keeping a release count across a new
361
+ * dataset would let its rows load all at once — the burst this gate exists to prevent.
362
+ * Progress is kept only for an appended page: the new dataset must repeat the previous
363
+ * items, in order, as its prefix, so the already-released rows still hold the same
364
+ * items and the new rows queue behind them. Any other replacement restarts the gate,
365
+ * including a same-length or larger array carrying different items.
366
+ */
367
+ private resetRowReleaseForNewData;
344
368
  /**
345
369
  * Returns the index of the last item whose rowStart is within maxRows.
346
370
  */
@@ -377,21 +401,18 @@ export default class CxBentoGrid extends CortexElement {
377
401
  * idempotent — safe to call again on already-observed elements.
378
402
  */
379
403
  private observeItemContentSlots;
404
+ /**
405
+ * Decides whether this pass restarts row gating, and does so.
406
+ *
407
+ * Must run BEFORE `computeLayout()` — see `resetRowRelease()`.
408
+ */
409
+ private applyRowReleaseResets;
380
410
  willUpdate(changed: PropertyValues): void;
381
411
  /**
382
412
  * Re-syncs row content heights whenever the layout recomputes (containerStyles
383
413
  * changes on every computeLayout() call, including resize-driven reflows) or
384
414
  * the sync-row-content-height flag itself is toggled.
385
415
  */
386
- /**
387
- * Restarts row gating when the dataset shrinks or is replaced wholesale.
388
- *
389
- * Row indices refer to a specific layout, so keeping a release count across a new
390
- * dataset would let its rows load all at once — the burst this gate exists to prevent.
391
- * A dataset that only grew is treated as an appended page: the already-released rows
392
- * still hold the same items, so their progress is kept and the new rows queue behind.
393
- */
394
- handleDataChangeForRowRelease(oldData?: unknown, newData?: unknown): void;
395
416
  handleRowContentHeightSync(): Promise<void>;
396
417
  /**
397
418
  * Get content position based on the size of the item.
@@ -328,6 +328,7 @@ export default class CxCarousel extends CortexElement {
328
328
  * Synchronizes the slides with the IntersectionObserver API.
329
329
  */
330
330
  private synchronizeSlides;
331
+ private synchronizeSlidesNow;
331
332
  private handleScrollEnd;
332
333
  private handleSlideContainerResize;
333
334
  private isCarouselItem;