@bicharts/chart-host 0.5.59 → 0.5.61

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.
package/dist/index.mjs CHANGED
@@ -651,6 +651,116 @@ function qualifyFailureFallsOpen(viaGenerate) {
651
651
  return viaGenerate === true;
652
652
  }
653
653
 
654
+ // src/qualifyFilter.ts
655
+ function normalizeFilterTerm(raw) {
656
+ if (typeof raw !== "string" || raw === "") return "";
657
+ return raw.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/\s+/g, " ").trim();
658
+ }
659
+ var FILTER_MIN_TERM_CHARS = 2;
660
+ function startsAWord(name, t) {
661
+ for (let i = name.indexOf(t); i >= 0; i = name.indexOf(t, i + 1)) {
662
+ if (i === 0) return true;
663
+ const prev = name.charCodeAt(i - 1);
664
+ const alnum = prev >= 97 && prev <= 122 || prev >= 48 && prev <= 57;
665
+ if (!alnum) return true;
666
+ }
667
+ return false;
668
+ }
669
+ function filterQualifyRows(rows, term, read) {
670
+ const all = Array.isArray(rows) ? rows.slice() : [];
671
+ const t = normalizeFilterTerm(term);
672
+ if (t.length < FILTER_MIN_TERM_CHARS) return { rows: all, tier: "all", term: "" };
673
+ const atWordStart = [];
674
+ const midWord = [];
675
+ const byDesc = [];
676
+ for (const row of all) {
677
+ const f = read(row);
678
+ const name = normalizeFilterTerm(f.name);
679
+ if (name === "") continue;
680
+ if (name.indexOf(t) >= 0) {
681
+ (startsAWord(name, t) ? atWordStart : midWord).push(row);
682
+ continue;
683
+ }
684
+ if (normalizeFilterTerm(f.description).indexOf(t) >= 0) byDesc.push(row);
685
+ }
686
+ if (atWordStart.length > 0) return { rows: atWordStart, tier: "name", term: t };
687
+ if (midWord.length > 0) return { rows: midWord, tier: "namePart", term: t };
688
+ if (byDesc.length > 0) return { rows: byDesc, tier: "desc", term: t };
689
+ return { rows: [], tier: "none", term: t };
690
+ }
691
+ var readQualifyChartRow = (r) => ({ name: r.name, description: r.description });
692
+ var readQualifyRefusalRow = (r) => ({ name: r.name, description: r.reason });
693
+ var FILTER_ROW_PX = 32;
694
+ var FILTER_MIN_WIDTH_PX = 420;
695
+ var FILTER_MIN_HEIGHT_PX = 300;
696
+ function filterFitsChooser(width, height) {
697
+ return Number.isFinite(width) && Number.isFinite(height) && width >= FILTER_MIN_WIDTH_PX && height >= FILTER_MIN_HEIGHT_PX;
698
+ }
699
+ function listNeedsFilter(scrollHeight, clientHeight) {
700
+ if (!Number.isFinite(scrollHeight) || !Number.isFinite(clientHeight)) return false;
701
+ return scrollHeight > clientHeight - FILTER_ROW_PX;
702
+ }
703
+ function qualifyFilterGate(i) {
704
+ if (!filterFitsChooser(i.cardWidth, i.cardHeight)) return { show: false, why: "too-small" };
705
+ if (!listNeedsFilter(i.scrollHeight, i.clientHeight)) return { show: false, why: "no-overflow" };
706
+ return { show: true, why: "shown" };
707
+ }
708
+ function computeQualifyFilterView(groups, term) {
709
+ const g = Array.isArray(groups) ? groups : [];
710
+ const read = (r) => ({ name: r.name, description: r.description });
711
+ const collect = (section) => {
712
+ const out = [];
713
+ for (const grp of g) if (grp.section === section) for (const r of grp.rows) out.push(r);
714
+ return out;
715
+ };
716
+ const fitRows = collect("fits");
717
+ const fit = filterQualifyRows(fitRows, term, read);
718
+ const ref = filterQualifyRows(collect("refused"), term, read);
719
+ const visible = /* @__PURE__ */ new Set();
720
+ for (const r of fit.rows) visible.add(r.el);
721
+ for (const r of ref.rows) visible.add(r.el);
722
+ const show = [], hide = [], showHeadings = [], hideHeadings = [];
723
+ for (const grp of g) {
724
+ let any = false;
725
+ for (const r of grp.rows) {
726
+ if (visible.has(r.el)) {
727
+ show.push(r.el);
728
+ any = true;
729
+ } else {
730
+ hide.push(r.el);
731
+ }
732
+ }
733
+ if (grp.heading !== null) (any ? showHeadings : hideHeadings).push(grp.heading);
734
+ }
735
+ const filtering = fit.tier !== "all";
736
+ const openRefusals = filtering && fit.rows.length === 0 && ref.rows.length > 0;
737
+ let note = "none";
738
+ if (filtering) {
739
+ if (fit.tier === "desc") note = "descFallback";
740
+ else if (fit.tier === "none") note = openRefusals ? "onlyRefusals" : "noMatch";
741
+ }
742
+ return {
743
+ show,
744
+ hide,
745
+ showHeadings,
746
+ hideHeadings,
747
+ tier: fit.tier,
748
+ matched: fit.rows.length,
749
+ total: fitRows.length,
750
+ refusalMatched: ref.rows.length,
751
+ openRefusals,
752
+ note,
753
+ term: fit.term
754
+ };
755
+ }
756
+ function qualifyFilterCountText(view) {
757
+ return view.tier === "all" ? String(view.total) : `${view.matched} of ${view.total}`;
758
+ }
759
+ function inlineFilterGate(rowCount) {
760
+ return rowCount >= INLINE_FILTER_MIN_ROWS ? { show: true, why: "shown" } : { show: false, why: "no-overflow" };
761
+ }
762
+ var INLINE_FILTER_MIN_ROWS = 8;
763
+
654
764
  // src/selectionCard.ts
655
765
  var SYNTHETIC_PREFIX = "__";
656
766
  var NUMERIC_DATATYPE = /int|double|decimal|single|float|number|currency|money/i;
@@ -893,10 +1003,15 @@ export {
893
1003
  CONTAINER_SLOT_XF_CLEAR,
894
1004
  DIM_OPACITY_DEFAULT,
895
1005
  DIM_OPACITY_VAR,
1006
+ FILTER_MIN_HEIGHT_PX,
1007
+ FILTER_MIN_TERM_CHARS,
1008
+ FILTER_MIN_WIDTH_PX,
1009
+ FILTER_ROW_PX,
896
1010
  FLIP_MODE_DEFAULT,
897
1011
  GEO_POINT_PRECISIONS,
898
1012
  HOST_CONTAINER_CLASS,
899
1013
  HOST_CONTRACT_VERSION,
1014
+ INLINE_FILTER_MIN_ROWS,
900
1015
  LEGEND_MARK_CLASS,
901
1016
  LIFT_SELECTED_CLASS,
902
1017
  MARK_CLASS,
@@ -921,34 +1036,44 @@ export {
921
1036
  clearGeoCache,
922
1037
  compileRenderFn,
923
1038
  compileTrivialSource,
1039
+ computeQualifyFilterView,
924
1040
  computeSelectionCard,
925
1041
  confirmLaunch,
926
1042
  createChartHost,
927
1043
  createMarkResolver,
928
1044
  ensureCrossfilterHitTargets,
929
1045
  explainRenderFailure,
1046
+ filterFitsChooser,
1047
+ filterQualifyRows,
930
1048
  geoAssetFor,
931
1049
  geoFromCache,
932
1050
  hasRefusalsToShow,
933
1051
  hitBandFlag,
1052
+ inlineFilterGate,
934
1053
  isBlankRender,
935
1054
  launchFavorStyle,
936
1055
  launchGenerates,
1056
+ listNeedsFilter,
937
1057
  loadGeo,
938
1058
  newQualifyGroupState,
939
1059
  newQualifyRefusalGroupState,
940
1060
  noopViewStateProvider,
941
1061
  normaliseAggregation,
1062
+ normalizeFilterTerm,
942
1063
  orderRefusalsForDisplay,
943
1064
  periodTickSuppressesFeedback,
944
1065
  planTrivialChart,
945
1066
  qualifyAuto,
946
1067
  qualifyCancel,
947
1068
  qualifyFailureFallsOpen,
1069
+ qualifyFilterCountText,
1070
+ qualifyFilterGate,
948
1071
  qualifyGroupHeadingFor,
949
1072
  qualifyPick,
950
1073
  qualifyRefusalHeadingFor,
951
1074
  rasterizeSvgToPngDataUrl,
1075
+ readQualifyChartRow,
1076
+ readQualifyRefusalRow,
952
1077
  refusalIsSelectable,
953
1078
  registerCityTable,
954
1079
  registerGeo,
@@ -13,6 +13,7 @@ export { shouldReview, buildReviewWire, bareBase64, actionFor, type ReviewGate,
13
13
  export { askApplyImprovements, type ReviewDialogOptions, type ReviewDialogText } from "./reviewDialog";
14
14
  export { qualifyGroupHeadingFor, newQualifyGroupState, type QualifyGroupRow, type QualifyGroupState, type QualifyGroupHeading, orderRefusalsForDisplay, refusalIsSelectable, hasRefusalsToShow, qualifyRefusalHeadingFor, newQualifyRefusalGroupState, type QualifyRefusalRow, type QualifyRefusalGroupState, type QualifyRefusalHeading, } from "./qualifyGroups";
15
15
  export { qualifyPick, qualifyAuto, qualifyCancel, launchGenerates, launchFavorStyle, chooserFitsViewport, shouldOpenChooserOnGenerate, shouldOpenInlineChooserOnGenerate, canConfirmLaunch, confirmLaunch, qualifyFailureFallsOpen, CHOOSER_MIN_WIDTH_PX, CHOOSER_MIN_HEIGHT_PX, type QualifyLaunchOutcome, type ChooserGateInput, } from "./qualifyLaunch";
16
+ export { normalizeFilterTerm, filterQualifyRows, readQualifyChartRow, readQualifyRefusalRow, filterFitsChooser, listNeedsFilter, qualifyFilterGate, inlineFilterGate, FILTER_MIN_TERM_CHARS, FILTER_ROW_PX, FILTER_MIN_WIDTH_PX, FILTER_MIN_HEIGHT_PX, INLINE_FILTER_MIN_ROWS, computeQualifyFilterView, qualifyFilterCountText, type QualifyFilterTier, type QualifyFilterResult, type QualifyFilterRead, type QualifyFilterGateReason, type QualifyFilterGateInput, type QualifyFilterRow, type QualifyFilterGroup, type QualifyFilterView, type QualifyFilterNoteKind, } from "./qualifyFilter";
16
17
  export { computeSelectionCard, normaliseAggregation, type SelectionCardModel, type SelectionCardLine, type SelectionCardOptions, } from "./selectionCard";
17
18
  export { ensureCrossfilterHitTargets, type HitTargetReport } from "./hitTargets";
18
19
  export { censusMarks, isBlankRender, blankRenderFlag, type MarkCensus, type BlankVerdictInput } from "./blankRender";
@@ -0,0 +1,270 @@
1
+ /**
2
+ * The comparison form of a term or a field.
3
+ *
4
+ * Accent folding is not decoration. The catalogue is English, but the reader's keyboard need not
5
+ * be, and a "Sankey" typed through a dead key has to match the same row a plain one does. NFD
6
+ * splits a composed character into base + combining marks and the range strip removes the marks,
7
+ * which is the whole of it - no transliteration, no stemming, nothing that could make two
8
+ * different chart names collide.
9
+ *
10
+ * Whitespace is collapsed rather than stripped: "bar chart" and "bar chart" are the same query,
11
+ * but "barchart" is deliberately NOT one. A reader who omits the space is asking for something we
12
+ * do not have a name for, and silently succeeding there would make the failures inexplicable.
13
+ */
14
+ export declare function normalizeFilterTerm(raw: string | null | undefined): string;
15
+ /**
16
+ * THE SHORTEST TERM THAT FILTERS. One character matches most of a 108-row catalogue, so the
17
+ * list would flicker through a near-identity pass and teach the reader nothing; below this the
18
+ * list is returned untouched and the host hides its counter.
19
+ */
20
+ export declare const FILTER_MIN_TERM_CHARS = 2;
21
+ /**
22
+ * WHICH TIER ANSWERED - the host needs this, not just the rows.
23
+ *
24
+ * "all" - no term (or too short). The rows are the input, unfiltered.
25
+ * "name" - the term starts a WORD in at least one name. Every lower tier is suppressed.
26
+ * "namePart" - it appears mid-word in a name, and nowhere at a word start.
27
+ * "desc" - no name matched at all, so the description fallback answered. THE HOST SAYS SO.
28
+ * "none" - nothing matched anywhere.
29
+ *
30
+ * A HOST ONLY HAS TO ANNOUNCE "desc". The first two are both name matches and need no
31
+ * explanation; the third is the one a reader would otherwise read as broken matching.
32
+ */
33
+ export type QualifyFilterTier = "all" | "name" | "namePart" | "desc" | "none";
34
+ export interface QualifyFilterResult<T> {
35
+ rows: T[];
36
+ tier: QualifyFilterTier;
37
+ /** The normalized term actually applied. Empty when the tier is "all". */
38
+ term: string;
39
+ }
40
+ /** How to read a row's two searchable fields. Kept as a callback because the fitting rows and
41
+ * the refused rows carry different field names and must not need two copies of this logic. */
42
+ export type QualifyFilterRead<T> = (row: T) => {
43
+ name?: string | null;
44
+ description?: string | null;
45
+ };
46
+ /**
47
+ * Filter `rows` by `term`, PRESERVING INPUT ORDER EXACTLY in every branch.
48
+ *
49
+ * THREE TIERS, AND THE FIRST NON-EMPTY ONE WINS OUTRIGHT. That single rule is what makes this
50
+ * feel like a name filter instead of a search box:
51
+ *
52
+ * 1. the term starts a WORD in the name - "gan" -> Gantt chart, and NOT Organization chart
53
+ * 2. it appears mid-word in the name - "eth" -> Choropleth, which tier 1 cannot reach
54
+ * 3. it appears in the description - "hierarchy" -> Organization chart
55
+ *
56
+ * Typing "time" returns `Time series plot` and NOT the dozen types whose descriptions happen to
57
+ * say "over time", which is what a flat name-or-description match produces and which reads,
58
+ * correctly, as broken.
59
+ *
60
+ * The description tier exists so a reader who types a word we carry in no NAME still finds
61
+ * something rather than an empty list, and it fires only when both name tiers are empty. When it
62
+ * fires the host has to say so ("No name matches - showing types whose description mentions
63
+ * 'stacked'"): an unannounced fallback is indistinguishable from bad matching. The two NAME tiers
64
+ * need no announcement - both are the reader's own word, found where they expected it.
65
+ *
66
+ * A row with no name is dropped from every tier. It cannot be labelled, so it cannot be chosen,
67
+ * and the hosts already drop it at render time.
68
+ */
69
+ export declare function filterQualifyRows<T>(rows: readonly T[] | null | undefined, term: string | null | undefined, read: QualifyFilterRead<T>): QualifyFilterResult<T>;
70
+ /** Reads a fitting row (`charts[]`): `name` / `description`. */
71
+ export declare const readQualifyChartRow: QualifyFilterRead<{
72
+ name?: string | null;
73
+ description?: string | null;
74
+ }>;
75
+ /**
76
+ * Reads a REFUSED row (`refused[]`). Its second field is the gate's SENTENCE rather than a
77
+ * description, and searching it is right for the same reason the tier exists: the sentence names
78
+ * the reader's own columns, so "region" finding every type refused over Region is a useful
79
+ * answer to "why isn't my chart here" - which is the only question that section is open to ask.
80
+ */
81
+ export declare const readQualifyRefusalRow: QualifyFilterRead<{
82
+ name?: string | null;
83
+ reason?: string | null;
84
+ }>;
85
+ /**
86
+ * THE HEIGHT THE FILTER ROW COSTS THE LIST - the row plus its 6px bottom margin.
87
+ *
88
+ * Measured, not assumed: 32px, and constant across all 195 tile sizes in the sweep below (the
89
+ * row is a flex line around a 0.85em input, so it does not reflow with the card).
90
+ */
91
+ export declare const FILTER_ROW_PX = 32;
92
+ /**
93
+ * THE SMALLEST CARD THE FILTER BOX IS USABLE IN - measured 2026-09-03, and NOT arithmetic off
94
+ * `CHOOSER_MIN_*`.
95
+ *
96
+ * The chooser's own floor (400 x 240) was measured with three pinned elements. The filter row is
97
+ * a FOURTH, in a card that clips, so the old envelope does not survive the addition and could not
98
+ * be adjusted by adding 32px to it - the coupling between the two dimensions moves too.
99
+ *
100
+ * Same harness as the 2026-09-01 chooser sweep, re-run over 195 sizes with the row present. The
101
+ * run reproduces the chooser's published coupling exactly when the row is hidden (320-340 wide
102
+ * needs 280 tall, 360-380 needs 260, 400+ needs 220), which is what says the harness is measuring
103
+ * the same card and not a different one.
104
+ *
105
+ * ONE CRITERION IS TIGHTER, and it is the reason this is a separate floor rather than a bigger
106
+ * version of the old one: the chooser sweep asked for TWO visible list rows, this asks for
107
+ * THREE. A filter that leaves two rows visible did not earn the height it cost.
108
+ *
109
+ * Measured envelope with the row present:
110
+ *
111
+ * 320-340 wide -> 380 tall 400 wide -> 300 tall 420+ wide -> 280 tall
112
+ *
113
+ * 420 x 300 is that envelope at its cheapest width, with one sweep step (20px) of margin on the
114
+ * height for the reason `CHOOSER_MIN_*` states: the measurement used one font stack and one
115
+ * string, every host localizes the title, and a longer translation wraps sooner than the sample.
116
+ *
117
+ * ERRING HIGH IS THE CHEAP DIRECTION, exactly as it is for the chooser. Below this floor the
118
+ * reader gets the chooser they have today, unchanged; above it wrongly, they get a clipped
119
+ * control in a card that cannot scroll to reveal it.
120
+ */
121
+ export declare const FILTER_MIN_WIDTH_PX = 420;
122
+ export declare const FILTER_MIN_HEIGHT_PX = 300;
123
+ /**
124
+ * Condition A: can this card afford the box at all?
125
+ *
126
+ * MEASURE THE REAL SURFACE - the card as drawn, never a viewport the host reports outward. The
127
+ * chooser's gate says the same thing for the same reason: where an author can state a viewport
128
+ * for generation purposes, that stated size describes a tile that does not exist yet.
129
+ */
130
+ export declare function filterFitsChooser(width: number, height: number): boolean;
131
+ /**
132
+ * Condition B: is the list long enough to be worth filtering?
133
+ *
134
+ * A filter box over four rows is clutter that spends pinned height for nothing, so the box
135
+ * appears only when the list actually scrolls.
136
+ *
137
+ * THE `- FILTER_ROW_PX` IS LOAD-BEARING, and leaving it out is how the naive version ships a
138
+ * flicker loop: inserting the box shrinks the list, which can turn a list that did not overflow
139
+ * into one that does, which would remove the box, which restores the overflow. Asking whether the
140
+ * content exceeds the height the list WOULD have is a single stable pass, because `scrollHeight`
141
+ * is content height and does not change when the container shrinks.
142
+ */
143
+ export declare function listNeedsFilter(scrollHeight: number, clientHeight: number): boolean;
144
+ /** Why the box is or is not on screen - logged once per open, so the floor can be judged against
145
+ * real tiles rather than only against the sweep. */
146
+ export type QualifyFilterGateReason = "shown" | "too-small" | "no-overflow";
147
+ export interface QualifyFilterGateInput {
148
+ /** Real drawn width of the CARD, in CSS pixels. */
149
+ cardWidth: number;
150
+ /** Real drawn height of the card. */
151
+ cardHeight: number;
152
+ /** The list's content height and its current viewport height. */
153
+ scrollHeight: number;
154
+ clientHeight: number;
155
+ }
156
+ /**
157
+ * Both conditions, and the reason - which the host logs whichever way it comes out.
158
+ *
159
+ * DECIDED ONCE PER OPEN. Neither host re-asks this while the dialog is up: not when the filter
160
+ * hides rows and the list stops overflowing (a control that vanishes mid-typing is worse than one
161
+ * that is briefly unnecessary), and not on resize (a dialog whose controls appear and disappear
162
+ * under a window drag reads as broken).
163
+ */
164
+ export declare function qualifyFilterGate(i: QualifyFilterGateInput): {
165
+ show: boolean;
166
+ why: QualifyFilterGateReason;
167
+ };
168
+ /** One row, as the hosts record it while BUILDING the list - the element handle plus the text it
169
+ * is matched on, carried beside the element rather than scraped back out of it later. */
170
+ export interface QualifyFilterRow<E> {
171
+ el: E;
172
+ name: string;
173
+ description: string;
174
+ }
175
+ /** A heading and the rows it introduces, in render order. `section` separates the fitting list
176
+ * from the refusal block behind "Show all chart types" - two independently visible surfaces
177
+ * answering two different questions, and only one of them can be auto-opened. */
178
+ export interface QualifyFilterGroup<E> {
179
+ heading: E | null;
180
+ section: "fits" | "refused";
181
+ rows: QualifyFilterRow<E>[];
182
+ }
183
+ /** WHICH SENTENCE THE HOST HAS TO SHOW. The host owns the words (it localizes); this owns the
184
+ * decision, which is the half that can differ between two hosts without anyone noticing.
185
+ *
186
+ * "none" - a name tier answered, or nothing is being filtered. Say nothing: the reader's
187
+ * own word was found where they expected it and needs no explanation.
188
+ * "descFallback" - no name matched; these matched on description. MUST be said - a silent
189
+ * fall-through is indistinguishable from bad matching.
190
+ * "onlyRefusals" - nothing that FITS matches, but something in the refusal block does. Say where
191
+ * it went; that block is now open.
192
+ * "noMatch" - nothing anywhere. NEVER phrase this like "nothing fits your data": one is a
193
+ * statement about a typed string and is fixed with backspace, the other is a
194
+ * statement about the data and is not.
195
+ */
196
+ export type QualifyFilterNoteKind = "none" | "descFallback" | "onlyRefusals" | "noMatch";
197
+ export interface QualifyFilterView<E> {
198
+ /** Rows to show, and rows to hide. Two lists rather than one predicate so a host can write
199
+ * them in one pass without asking a Set per element. */
200
+ show: E[];
201
+ hide: E[];
202
+ /** Headings to show, and headings that now introduce nothing. A heading with nothing under it
203
+ * is a label for an empty category, and the rule it draws reads as a divider between two
204
+ * things that are not there. */
205
+ showHeadings: E[];
206
+ hideHeadings: E[];
207
+ tier: QualifyFilterTier;
208
+ /** Matches and totals for the FITTING list - what the "12 of 137" counter is made of. */
209
+ matched: number;
210
+ total: number;
211
+ refusalMatched: number;
212
+ /**
213
+ * Does the refusal block need to be OPEN for this term?
214
+ *
215
+ * True only when the term matches nothing that fits and something that does not. "Why isn't
216
+ * my chart here" is the only question that section is open to ask, and the reader has just
217
+ * asked it by name - the server's own sentence about why it cannot be drawn is already
218
+ * rendered, one collapsed checkbox away.
219
+ *
220
+ * THE HOST MUST TREAT THIS AS AN OVERRIDE, NOT A SETTING: remember what the reader's own
221
+ * checkbox said before the first override, and put it back the moment this goes false. An
222
+ * auto-expansion that sticks leaves the reader with a section they never opened.
223
+ */
224
+ openRefusals: boolean;
225
+ note: QualifyFilterNoteKind;
226
+ /** The normalized term, for the host to interpolate into whichever sentence it shows. */
227
+ term: string;
228
+ }
229
+ /**
230
+ * The filtered view of a whole dialog: fitting rows, refusal rows, headings and the note.
231
+ *
232
+ * ORDER IS UNTOUCHED, as everywhere else here - `show` and `hide` come out in the order the
233
+ * groups were built, so a host writing them in sequence cannot reorder anything by accident.
234
+ */
235
+ export declare function computeQualifyFilterView<E>(groups: readonly QualifyFilterGroup<E>[], term: string | null | undefined): QualifyFilterView<E>;
236
+ /** The counter beside the box: "12 of 137" while filtering, the plain total otherwise.
237
+ *
238
+ * Digits only, assembled here so both hosts show the same shape of number - the words around it
239
+ * (there are none today) would be the host's business, the arithmetic is not. */
240
+ export declare function qualifyFilterCountText(view: {
241
+ tier: QualifyFilterTier;
242
+ matched: number;
243
+ total: number;
244
+ }): string;
245
+ /**
246
+ * The same question for a host whose chooser is an INLINE, SCROLLING PANEL - and it has no size
247
+ * clause, for the reason `shouldOpenInlineChooserOnGenerate` already sets out at length.
248
+ *
249
+ * THE ASYMMETRY IS THE POINT AND IT IS MEASURED. The modal lives in a card that CLIPS, so past a
250
+ * certain smallness its controls are simply not on screen. A panel that flows inside a scrolling
251
+ * pane, with a wrapping footer and a list carrying its own max-height, cannot reach that state -
252
+ * swept across 63 pane sizes down to 200x120, every one stayed usable. A size clause here would
253
+ * protect nobody and would switch the feature off in a default Excel task pane, which is narrower
254
+ * than the modal's floor.
255
+ *
256
+ * The OVERFLOW half still applies: a five-row answer does not want a filter box in any host.
257
+ */
258
+ export declare function inlineFilterGate(rowCount: number): {
259
+ show: boolean;
260
+ why: QualifyFilterGateReason;
261
+ };
262
+ /**
263
+ * The inline panel's stand-in for "the list scrolls".
264
+ *
265
+ * A COUNT RATHER THAN A MEASUREMENT, because the panel's list carries a fixed `max-height: 220px`
266
+ * and rows are one line each - so the count IS the overflow question there, where in the modal
267
+ * the card's height is the variable and the count is not. Eight rows at ~24px is the point the
268
+ * 220px list starts scrolling; below it there is nothing to scroll past.
269
+ */
270
+ export declare const INLINE_FILTER_MIN_ROWS = 8;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.5.59",
3
+ "version": "0.5.61",
4
4
  "description": "Run a BIC-generated D3 chart in any web host: compiles the generated render() function, applies the shared option defaults, resolves mark clicks (through tooltip overlays), owns the selection affordance, and translates row indices between cross-filtered charts. The same contract the BIC Power BI visual implements, minus Power BI. React bindings at @bicharts/chart-host/react.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",