@bicharts/chart-host 0.5.105 → 0.5.107

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
@@ -649,6 +649,140 @@ function askApplyImprovements(container, reason, opts = {}) {
649
649
  });
650
650
  }
651
651
 
652
+ // src/productFeedback.ts
653
+ var PRODUCT_FEEDBACK_MAX_CHARS = 2e3;
654
+ var PRODUCT_FEEDBACK_MIN_CHARS = 3;
655
+ function normaliseProductFeedbackText(raw, maxChars = PRODUCT_FEEDBACK_MAX_CHARS) {
656
+ const s2 = String(raw ?? "").replace(/\r\n?/g, "\n").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "").split("\n").map((line) => line.replace(/[ \t]+$/, "")).join("\n").replace(/\n{3,}/g, "\n\n").trim();
657
+ return s2.length > maxChars ? s2.slice(0, maxChars).trimEnd() : s2;
658
+ }
659
+ function productFeedbackSendable(raw, maxChars = PRODUCT_FEEDBACK_MAX_CHARS) {
660
+ return normaliseProductFeedbackText(raw, maxChars).length >= PRODUCT_FEEDBACK_MIN_CHARS;
661
+ }
662
+ function buildProductFeedbackPayload(input) {
663
+ const id = input.identity ?? {};
664
+ return {
665
+ text: normaliseProductFeedbackText(input.text),
666
+ host: input.host,
667
+ surface: input.surface || "landing",
668
+ clientVersion: input.clientVersion || "",
669
+ correlationId: input.correlationId || "",
670
+ clientId: id.clientId || "",
671
+ licenseKey: id.licenseKey || "",
672
+ licensee: id.licensee || "",
673
+ secretKey: id.secretKey || "",
674
+ freemiumKey: id.freemiumKey || "",
675
+ linkNonce: id.linkNonce || "",
676
+ instanceKey: id.instanceKey || ""
677
+ };
678
+ }
679
+ var DEFAULT_TEXT2 = {
680
+ label: "Your suggestions",
681
+ placeholder: "An idea, a missing chart, or something that got in your way",
682
+ hint: "Sent to our team. It is never used to draw a chart.",
683
+ send: "Send",
684
+ sending: "Sending...",
685
+ sent: "Thank you - your suggestion was sent.",
686
+ failed: "Your suggestion could not be sent. It is still in the box - please try again."
687
+ };
688
+ function mountProductFeedbackBox(container, opts) {
689
+ const doc = container.ownerDocument ?? document;
690
+ const text = { ...DEFAULT_TEXT2, ...opts.text ?? {} };
691
+ const maxChars = Math.max(PRODUCT_FEEDBACK_MIN_CHARS, Math.floor(opts.maxChars ?? PRODUCT_FEEDBACK_MAX_CHARS));
692
+ const wrap = doc.createElement("div");
693
+ wrap.className = "bic-product-feedback" + (opts.className ? " " + opts.className : "");
694
+ const inputId = "bic-product-feedback-" + ++mounted;
695
+ const label = doc.createElement("label");
696
+ label.className = "bic-product-feedback-label";
697
+ label.htmlFor = inputId;
698
+ label.textContent = text.label;
699
+ const input = doc.createElement("textarea");
700
+ input.className = "bic-product-feedback-input";
701
+ input.id = inputId;
702
+ input.rows = 2;
703
+ input.maxLength = maxChars;
704
+ input.placeholder = text.placeholder;
705
+ input.setAttribute("aria-describedby", inputId + "-hint");
706
+ const hint = doc.createElement("div");
707
+ hint.className = "bic-product-feedback-hint";
708
+ hint.id = inputId + "-hint";
709
+ hint.textContent = text.hint;
710
+ const row = doc.createElement("div");
711
+ row.className = "bic-product-feedback-row";
712
+ const button = doc.createElement("button");
713
+ button.type = "button";
714
+ button.className = "bic-product-feedback-send";
715
+ button.textContent = text.send;
716
+ const status = doc.createElement("span");
717
+ status.className = "bic-product-feedback-status";
718
+ status.setAttribute("role", "status");
719
+ status.setAttribute("aria-live", "polite");
720
+ row.append(button, status);
721
+ wrap.append(label, input, hint, row);
722
+ container.appendChild(wrap);
723
+ let sending = false;
724
+ const sync = () => {
725
+ button.disabled = sending || !productFeedbackSendable(input.value, maxChars);
726
+ };
727
+ const onInput = () => {
728
+ if (!sending && status.textContent) {
729
+ status.textContent = "";
730
+ status.removeAttribute("data-state");
731
+ }
732
+ sync();
733
+ };
734
+ const onClick = async () => {
735
+ if (sending || !productFeedbackSendable(input.value, maxChars)) return;
736
+ const message = normaliseProductFeedbackText(input.value, maxChars);
737
+ sending = true;
738
+ sync();
739
+ button.textContent = text.sending;
740
+ status.textContent = "";
741
+ status.removeAttribute("data-state");
742
+ let result;
743
+ let failure = "";
744
+ try {
745
+ result = await opts.send(message);
746
+ if (!result?.ok) failure = result?.message || "not sent";
747
+ } catch (e2) {
748
+ result = { ok: false };
749
+ failure = String(e2?.message ?? e2);
750
+ }
751
+ sending = false;
752
+ button.textContent = text.send;
753
+ if (result?.ok) {
754
+ input.value = "";
755
+ status.textContent = result.message || text.sent;
756
+ status.setAttribute("data-state", "sent");
757
+ safely(() => opts.onSent?.());
758
+ } else {
759
+ status.textContent = result?.message || text.failed;
760
+ status.setAttribute("data-state", "failed");
761
+ safely(() => opts.onFailed?.(failure));
762
+ }
763
+ sync();
764
+ };
765
+ input.addEventListener("input", onInput);
766
+ button.addEventListener("click", onClick);
767
+ sync();
768
+ safely(() => opts.onShown?.());
769
+ return {
770
+ element: wrap,
771
+ destroy() {
772
+ input.removeEventListener("input", onInput);
773
+ button.removeEventListener("click", onClick);
774
+ wrap.remove();
775
+ }
776
+ };
777
+ }
778
+ var mounted = 0;
779
+ function safely(fn) {
780
+ try {
781
+ fn();
782
+ } catch {
783
+ }
784
+ }
785
+
652
786
  // src/qualifyGroups.ts
653
787
  function newQualifyGroupState() {
654
788
  return { preview: false, previewClosed: false, projected: false, notRecommended: false, rankedSeen: false };
@@ -934,6 +1068,14 @@ function cleanFormat(raw, dialect) {
934
1068
  if (!s2.trim() || /^general$/i.test(s2.trim())) return null;
935
1069
  return hasDateToken(s2, dialect) ? s2 : null;
936
1070
  }
1071
+ function isExcelDateFormat(raw) {
1072
+ if (!raw) return false;
1073
+ const bare = stripLiterals(firstSection(String(raw), "excel"), "excel");
1074
+ if (/\[(h+|m+|s+)\]/i.test(bare)) return false;
1075
+ const tokens = bare.replace(/\[[^\]]*\]/g, "").trim();
1076
+ if (!tokens || /^general$/i.test(tokens)) return false;
1077
+ return /[ymdhs]/i.test(tokens);
1078
+ }
937
1079
  function firstSection(s2, dialect) {
938
1080
  let out = "";
939
1081
  let quote = "";
@@ -971,6 +1113,10 @@ function stripLiterals(s2, dialect) {
971
1113
  i2++;
972
1114
  continue;
973
1115
  }
1116
+ if (dialect === "excel" && (ch === "_" || ch === "*")) {
1117
+ i2++;
1118
+ continue;
1119
+ }
974
1120
  if (ch === '"' || dialect === "dotnet" && ch === "'") {
975
1121
  const end = s2.indexOf(ch, i2 + 1);
976
1122
  i2 = end < 0 ? s2.length : end;
@@ -2270,6 +2416,8 @@ export {
2270
2416
  PILL_MIN_ALPHA,
2271
2417
  PILL_MIN_COVERAGE,
2272
2418
  PILL_OPAQUE_ALPHA,
2419
+ PRODUCT_FEEDBACK_MAX_CHARS,
2420
+ PRODUCT_FEEDBACK_MIN_CHARS,
2273
2421
  QUALIFY_REFUSAL_UNSPECIFIED,
2274
2422
  ROW_IDX_ATTR,
2275
2423
  SAME_SHADE_DELTA_E,
@@ -2291,6 +2439,7 @@ export {
2291
2439
  blankRenderFlag,
2292
2440
  buildPalette,
2293
2441
  buildPaletteFromSeed,
2442
+ buildProductFeedbackPayload,
2294
2443
  buildRenderPayload,
2295
2444
  buildReviewWire,
2296
2445
  canConfirmLaunch,
@@ -2337,6 +2486,7 @@ export {
2337
2486
  inlineFilterGate,
2338
2487
  isBlankRender,
2339
2488
  isBlindMarker,
2489
+ isExcelDateFormat,
2340
2490
  isHorizontalLabelRow,
2341
2491
  isPhantomBox,
2342
2492
  isPillBackdropAlpha,
@@ -2349,11 +2499,13 @@ export {
2349
2499
  loadGeo,
2350
2500
  markerArmedBlind,
2351
2501
  measureContainerBoxes,
2502
+ mountProductFeedbackBox,
2352
2503
  needsScroll,
2353
2504
  newQualifyGroupState,
2354
2505
  newQualifyRefusalGroupState,
2355
2506
  noopViewStateProvider,
2356
2507
  normaliseAggregation,
2508
+ normaliseProductFeedbackText,
2357
2509
  normalizeFilterTerm,
2358
2510
  orderMostDistinct,
2359
2511
  orderRefusalsForDisplay,
@@ -2368,6 +2520,7 @@ export {
2368
2520
  planFrameGrow,
2369
2521
  planTrivialChart,
2370
2522
  pollGiveUpKeepsMarker,
2523
+ productFeedbackSendable,
2371
2524
  qualifyAuto,
2372
2525
  qualifyCancel,
2373
2526
  qualifyFailureFallsOpen,
@@ -12,11 +12,12 @@ export { planTrivialChart, compileTrivialSource, type TrivialPlan, type TrivialS
12
12
  export { captureSvgSnapshot, svgToDataUrl, svgNaturalSize, rasterizeSvgToPngDataUrl, type SnapshotOptions } from "./snapshot";
13
13
  export { shouldReview, buildReviewWire, bareBase64, actionFor, type ReviewGate, type ReviewWire, type ReviewVerdict, type ReviewAction } from "./review";
14
14
  export { askApplyImprovements, type ReviewDialogOptions, type ReviewDialogText } from "./reviewDialog";
15
+ export { mountProductFeedbackBox, normaliseProductFeedbackText, productFeedbackSendable, buildProductFeedbackPayload, PRODUCT_FEEDBACK_MAX_CHARS, PRODUCT_FEEDBACK_MIN_CHARS, type ProductFeedbackBox, type ProductFeedbackBoxOptions, type ProductFeedbackText, type ProductFeedbackSendResult, type ProductFeedbackPayload, type ProductFeedbackIdentity, type ProductFeedbackHost, } from "./productFeedback";
15
16
  export { qualifyGroupHeadingFor, newQualifyGroupState, type QualifyGroupRow, type QualifyGroupState, type QualifyGroupHeading, orderRefusalsForDisplay, refusalIsSelectable, hasRefusalsToShow, qualifyRefusalHeadingFor, newQualifyRefusalGroupState, qualifyRefusalReason, QUALIFY_REFUSAL_UNSPECIFIED, type QualifyRefusalRow, type QualifyRefusalGroupState, type QualifyRefusalHeading, } from "./qualifyGroups";
16
17
  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";
17
18
  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";
18
19
  export { computeSelectionCard, normaliseAggregation, type SelectionCardModel, type SelectionCardLine, type SelectionCardOptions, } from "./selectionCard";
19
- export { formatSourceDate, formatSourceDateFor, type SourceFormats, type SourceFormatDialect, type SourceDateOptions, } from "./sourceDateFormat";
20
+ export { formatSourceDate, formatSourceDateFor, isExcelDateFormat, type SourceFormats, type SourceFormatDialect, type SourceDateOptions, } from "./sourceDateFormat";
20
21
  export { composeSummaryText } from "./summaryText";
21
22
  export { ensureCrossfilterHitTargets, type HitTargetReport } from "./hitTargets";
22
23
  export { applyLabelContrast, LABEL_CONTRAST_DONE_ATTR, LABEL_CONTRAST_CAP, type LabelContrastOptions, type LabelContrastReport, } from "./labelContrastDom";
@@ -0,0 +1,88 @@
1
+ /** The longest message a host will send. Mirrored by the service's own cap. */
2
+ export declare const PRODUCT_FEEDBACK_MAX_CHARS = 2000;
3
+ /** Below this there is nothing a person could act on ("ok", "hi"). */
4
+ export declare const PRODUCT_FEEDBACK_MIN_CHARS = 3;
5
+ /** Which host a message came from. The service files it under that host. */
6
+ export type ProductFeedbackHost = "PBI" | "EXCEL";
7
+ /**
8
+ * Clean a typed message for sending: control characters other than line breaks and tabs removed,
9
+ * trailing spaces on each line removed, runs of blank lines collapsed to one, trimmed, and capped.
10
+ * Pure, so every host sends the same bytes for the same keystrokes.
11
+ */
12
+ export declare function normaliseProductFeedbackText(raw: string | null | undefined, maxChars?: number): string;
13
+ /** True when a message is long enough to send. */
14
+ export declare function productFeedbackSendable(raw: string | null | undefined, maxChars?: number): boolean;
15
+ /** The credentials a host already holds, forwarded so the service can attach an account when there is one. The
16
+ * service resolves identity itself; nothing here names an account. Absent fields are sent empty. */
17
+ export interface ProductFeedbackIdentity {
18
+ licenseKey?: string;
19
+ licensee?: string;
20
+ secretKey?: string;
21
+ freemiumKey?: string;
22
+ linkNonce?: string;
23
+ instanceKey?: string;
24
+ clientId?: string;
25
+ }
26
+ export interface ProductFeedbackPayload {
27
+ text: string;
28
+ host: ProductFeedbackHost;
29
+ surface: string;
30
+ clientVersion: string;
31
+ correlationId: string;
32
+ clientId: string;
33
+ licenseKey: string;
34
+ licensee: string;
35
+ secretKey: string;
36
+ freemiumKey: string;
37
+ linkNonce: string;
38
+ instanceKey: string;
39
+ }
40
+ /** The request body every host sends (before its own encoding and signing). */
41
+ export declare function buildProductFeedbackPayload(input: {
42
+ text: string;
43
+ host: ProductFeedbackHost;
44
+ surface?: string;
45
+ clientVersion: string;
46
+ correlationId: string;
47
+ identity?: ProductFeedbackIdentity;
48
+ }): ProductFeedbackPayload;
49
+ /** What a host's send callback reports. `message` is shown to the reader when present. */
50
+ export interface ProductFeedbackSendResult {
51
+ ok: boolean;
52
+ message?: string;
53
+ }
54
+ /** The words a reader sees. Hosts localize; every field has an English default. */
55
+ export interface ProductFeedbackText {
56
+ label?: string;
57
+ placeholder?: string;
58
+ hint?: string;
59
+ send?: string;
60
+ sending?: string;
61
+ sent?: string;
62
+ failed?: string;
63
+ }
64
+ export interface ProductFeedbackBoxOptions {
65
+ /** Delivers the cleaned text. Resolve { ok: false } (or throw) on failure; the box keeps the text either way. */
66
+ send: (text: string) => Promise<ProductFeedbackSendResult>;
67
+ text?: ProductFeedbackText;
68
+ /** Added to the wrapper so the host can skin it; the structure stays the same. */
69
+ className?: string;
70
+ maxChars?: number;
71
+ /** Called once, when the box is mounted - the host's "shown" counter. */
72
+ onShown?: () => void;
73
+ /** Called after a successful send - the host's "sent" counter. */
74
+ onSent?: () => void;
75
+ /** Called after a failed send, with what failed, so the host can log it. */
76
+ onFailed?: (reason: string) => void;
77
+ }
78
+ export interface ProductFeedbackBox {
79
+ element: HTMLElement;
80
+ /** Removes the box and its listeners. */
81
+ destroy(): void;
82
+ }
83
+ /**
84
+ * Build the box inside `container`. The wrapper carries `bic-product-feedback` (plus the host's
85
+ * class); the textarea, button and status line carry `bic-product-feedback-input`, `-send` and
86
+ * `-status`, so hosts and tests can find them without depending on the order of elements.
87
+ */
88
+ export declare function mountProductFeedbackBox(container: HTMLElement, opts: ProductFeedbackBoxOptions): ProductFeedbackBox;
@@ -28,3 +28,22 @@ export interface SourceDateOptions {
28
28
  export declare function formatSourceDate(raw: any, opts?: SourceDateOptions): string | null;
29
29
  /** Convenience for a caller holding a `SourceFormats` map and a column name. */
30
30
  export declare function formatSourceDateFor(raw: any, columnName: string, formats: SourceFormats | null | undefined, culture?: string): string | null;
31
+ /**
32
+ * Does an EXCEL number format make the cell a date? The question a host asks before it turns a
33
+ * serial number into a Date (2026-09-16).
34
+ *
35
+ * Excel stores 2026-03-01 as 46082 and only the format says which one it is, so reading a format
36
+ * wrongly in EITHER direction corrupts a column without an error anywhere. A bare letter test over
37
+ * the raw string gets it wrong in the direction that hurts: `0.00;[Red]0.00` - Excel's own built-in
38
+ * "Number, red negatives" - has a `d` in its colour, `[$USD] #,##0.00` an `s` in its currency tag,
39
+ * and a revenue of 1,234.50 read through either became a date in May 1903.
40
+ *
41
+ * So the format is reduced the way `cleanFormat` reduces it before rendering: the FIRST section (the
42
+ * one a positive serial takes), literals removed, bracket decorations removed. Only then does a
43
+ * remaining `y m d h s` make it a date.
44
+ *
45
+ * ELAPSED TIME IS A DURATION, NOT A DATE. `[h]:mm:ss` counts hours past 24: its value is a length of
46
+ * time, and turning it into an instant in January 1900 is the same trap as `0.00" days"`. A section
47
+ * carrying an elapsed bracket answers false, and the column stays a number.
48
+ */
49
+ export declare function isExcelDateFormat(raw: string | null | undefined): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.5.105",
3
+ "version": "0.5.107",
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",