@bicharts/chart-host 0.5.106 → 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 };
@@ -2282,6 +2416,8 @@ export {
2282
2416
  PILL_MIN_ALPHA,
2283
2417
  PILL_MIN_COVERAGE,
2284
2418
  PILL_OPAQUE_ALPHA,
2419
+ PRODUCT_FEEDBACK_MAX_CHARS,
2420
+ PRODUCT_FEEDBACK_MIN_CHARS,
2285
2421
  QUALIFY_REFUSAL_UNSPECIFIED,
2286
2422
  ROW_IDX_ATTR,
2287
2423
  SAME_SHADE_DELTA_E,
@@ -2303,6 +2439,7 @@ export {
2303
2439
  blankRenderFlag,
2304
2440
  buildPalette,
2305
2441
  buildPaletteFromSeed,
2442
+ buildProductFeedbackPayload,
2306
2443
  buildRenderPayload,
2307
2444
  buildReviewWire,
2308
2445
  canConfirmLaunch,
@@ -2362,11 +2499,13 @@ export {
2362
2499
  loadGeo,
2363
2500
  markerArmedBlind,
2364
2501
  measureContainerBoxes,
2502
+ mountProductFeedbackBox,
2365
2503
  needsScroll,
2366
2504
  newQualifyGroupState,
2367
2505
  newQualifyRefusalGroupState,
2368
2506
  noopViewStateProvider,
2369
2507
  normaliseAggregation,
2508
+ normaliseProductFeedbackText,
2370
2509
  normalizeFilterTerm,
2371
2510
  orderMostDistinct,
2372
2511
  orderRefusalsForDisplay,
@@ -2381,6 +2520,7 @@ export {
2381
2520
  planFrameGrow,
2382
2521
  planTrivialChart,
2383
2522
  pollGiveUpKeepsMarker,
2523
+ productFeedbackSendable,
2384
2524
  qualifyAuto,
2385
2525
  qualifyCancel,
2386
2526
  qualifyFailureFallsOpen,
@@ -12,6 +12,7 @@ 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";
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.5.106",
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",