@bicharts/chart-host 0.5.28 → 0.5.30

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
@@ -446,6 +446,129 @@ function actionFor(v) {
446
446
  return { kind: "keep", reason: v.errorMessage ?? v.verdictReason ?? "review failed", userVisible: false };
447
447
  }
448
448
 
449
+ // src/reviewDialog.ts
450
+ var DEFAULT_TEXT = {
451
+ title: "Apply improvements?",
452
+ fallbackReason: "The AI review suggests a change to this chart.",
453
+ cost: "Applying creates a new chart version and uses a generation. Declining costs nothing.",
454
+ decline: "No thanks",
455
+ accept: "Apply"
456
+ };
457
+ function askApplyImprovements(container, reason, opts = {}) {
458
+ return new Promise((resolve) => {
459
+ try {
460
+ const minW = opts.minWidth ?? 260;
461
+ const minH = opts.minHeight ?? 170;
462
+ const w = container.clientWidth || container.offsetWidth || 0;
463
+ const h = container.clientHeight || container.offsetHeight || 0;
464
+ if (w < minW || h < minH) {
465
+ resolve(false);
466
+ return;
467
+ }
468
+ const text = { ...DEFAULT_TEXT, ...opts.text ?? {} };
469
+ const doc = container.ownerDocument;
470
+ const ov = doc.createElement("div");
471
+ ov.id = opts.id ?? "bic-review-ask-overlay";
472
+ if (opts.overlayClass) ov.className = opts.overlayClass;
473
+ else ov.style.cssText = "position:absolute;inset:0;background:rgba(0,0,0,0.55);z-index:110;";
474
+ ov.style.display = "flex";
475
+ ov.style.alignItems = "center";
476
+ ov.style.justifyContent = "center";
477
+ const card = doc.createElement("div");
478
+ if (opts.cardClass) card.className = opts.cardClass;
479
+ else card.style.cssText = "background:#ffffff;color:#1f2937;border-radius:10px;box-shadow:0 6px 24px rgba(0,0,0,0.22);";
480
+ card.style.width = "auto";
481
+ card.style.height = "auto";
482
+ card.style.maxWidth = "min(420px, 90%)";
483
+ card.style.maxHeight = "90%";
484
+ card.style.overflow = "auto";
485
+ card.style.padding = "16px 18px";
486
+ card.style.display = "flex";
487
+ card.style.flexDirection = "column";
488
+ card.style.gap = "10px";
489
+ card.style.boxSizing = "border-box";
490
+ const head = doc.createElement("div");
491
+ head.style.cssText = "font:600 13px 'Segoe UI',system-ui,sans-serif;";
492
+ head.textContent = text.title;
493
+ const why = doc.createElement("div");
494
+ why.style.cssText = "font:400 12px/1.45 'Segoe UI',system-ui,sans-serif;white-space:normal;";
495
+ why.textContent = reason || text.fallbackReason;
496
+ const cost = doc.createElement("div");
497
+ cost.style.cssText = "font:400 11px/1.4 'Segoe UI',system-ui,sans-serif;opacity:0.75;white-space:normal;";
498
+ cost.textContent = text.cost;
499
+ const row = doc.createElement("div");
500
+ row.style.cssText = "display:flex;gap:8px;justify-content:flex-end;margin-top:4px;";
501
+ const no = doc.createElement("button");
502
+ no.type = "button";
503
+ no.textContent = text.decline;
504
+ const yes = doc.createElement("button");
505
+ yes.type = "button";
506
+ yes.textContent = text.accept;
507
+ yes.style.cssText = "font-weight:600;";
508
+ let done = false;
509
+ const finish = (v) => {
510
+ if (done) return;
511
+ done = true;
512
+ try {
513
+ if (ov.parentElement) ov.parentElement.removeChild(ov);
514
+ } catch {
515
+ }
516
+ doc.removeEventListener("keydown", onKey, true);
517
+ resolve(v);
518
+ };
519
+ const onKey = (e) => {
520
+ if (e.key === "Escape") {
521
+ e.stopPropagation();
522
+ finish(false);
523
+ }
524
+ };
525
+ no.addEventListener("click", () => finish(false));
526
+ yes.addEventListener("click", () => finish(true));
527
+ ov.addEventListener("click", (e) => {
528
+ if (e.target === ov) finish(false);
529
+ });
530
+ card.addEventListener("click", (e) => e.stopPropagation());
531
+ doc.addEventListener("keydown", onKey, true);
532
+ row.append(no, yes);
533
+ card.append(head, why, cost, row);
534
+ ov.appendChild(card);
535
+ container.appendChild(ov);
536
+ try {
537
+ yes.focus();
538
+ } catch {
539
+ }
540
+ } catch {
541
+ resolve(false);
542
+ }
543
+ });
544
+ }
545
+
546
+ // src/qualifyGroups.ts
547
+ function newQualifyGroupState() {
548
+ return { preview: false, previewClosed: false, projected: false, notRecommended: false };
549
+ }
550
+ function qualifyGroupHeadingFor(row, state) {
551
+ const preview = row.isPreview === true;
552
+ if (preview && !state.preview && !state.notRecommended) {
553
+ state.preview = true;
554
+ return "preview";
555
+ }
556
+ if (state.preview && !state.previewClosed && !preview) {
557
+ state.previewClosed = true;
558
+ return "main";
559
+ }
560
+ const projected = typeof row.viaProjection === "string" && row.viaProjection !== "";
561
+ if (projected && !state.projected && !state.notRecommended) {
562
+ state.projected = true;
563
+ return "projected";
564
+ }
565
+ if (row.recommended === false && !state.notRecommended) {
566
+ state.notRecommended = true;
567
+ return "notRecommended";
568
+ }
569
+ return null;
570
+ }
571
+
449
572
  // src/index.ts
450
573
  function registerCityTable(packed) {
451
574
  L3(packed);
@@ -479,6 +602,7 @@ export {
479
602
  SELECTION_ACTIVE_CLASS,
480
603
  XFILTER_REFRESH_EVENT,
481
604
  actionFor,
605
+ askApplyImprovements,
482
606
  bareBase64,
483
607
  buildRenderPayload,
484
608
  buildReviewWire,
@@ -492,7 +616,9 @@ export {
492
616
  geoAssetFor,
493
617
  geoFromCache,
494
618
  loadGeo,
619
+ newQualifyGroupState,
495
620
  planTrivialChart,
621
+ qualifyGroupHeadingFor,
496
622
  rasterizeSvgToPngDataUrl,
497
623
  registerCityTable,
498
624
  registerGeo,
@@ -10,3 +10,5 @@ export { createMarkResolver, type MarkResolver, type MarkResolverEnv } from "./s
10
10
  export { planTrivialChart, compileTrivialSource, type TrivialPlan, type TrivialShapeKind } from "./trivial";
11
11
  export { captureSvgSnapshot, svgToDataUrl, svgNaturalSize, rasterizeSvgToPngDataUrl, type SnapshotOptions } from "./snapshot";
12
12
  export { shouldReview, buildReviewWire, bareBase64, actionFor, type ReviewGate, type ReviewWire, type ReviewVerdict, type ReviewAction } from "./review";
13
+ export { askApplyImprovements, type ReviewDialogOptions, type ReviewDialogText } from "./reviewDialog";
14
+ export { qualifyGroupHeadingFor, newQualifyGroupState, type QualifyGroupRow, type QualifyGroupState, type QualifyGroupHeading, } from "./qualifyGroups";
@@ -0,0 +1,31 @@
1
+ /** One row of a qualify result, in the only shape this logic cares about. */
2
+ export interface QualifyGroupRow {
3
+ /** Newer type, deliberately unscorable. Server-stamped from the chart catalogue. */
4
+ isPreview?: boolean | null;
5
+ /** false = the ontology says a required channel cannot be satisfied by these fields. */
6
+ recommended?: boolean | null;
7
+ /** Non-empty when the type qualifies only against an AGGREGATED projection of the shape. */
8
+ viaProjection?: string | null;
9
+ }
10
+ /** Which heading to write BEFORE a row, if any. */
11
+ export type QualifyGroupHeading = "preview" | "main" | "projected" | "notRecommended";
12
+ /** Carried across the loop. Create one per render with `newQualifyGroupState()`. */
13
+ export interface QualifyGroupState {
14
+ preview: boolean;
15
+ previewClosed: boolean;
16
+ projected: boolean;
17
+ notRecommended: boolean;
18
+ }
19
+ export declare function newQualifyGroupState(): QualifyGroupState;
20
+ /**
21
+ * The heading (if any) that belongs immediately before `row`, MUTATING `state` so the caller's
22
+ * loop stays a plain for-of. At most one heading per row: the boundaries cannot coincide, since
23
+ * a row leaving the preview block is by definition not the row that opened it.
24
+ *
25
+ * ORDER OF THE CHECKS IS THE CONTRACT:
26
+ * 1. "preview" opens the block - but never once the not-recommended block has opened, because a
27
+ * preview type the ontology ruled out is NOT floated and belongs where it landed.
28
+ * 2. "main" closes it, at the first row that is not preview. Emitted only if a block opened.
29
+ * 3. "projected" and 4. "notRecommended" are unchanged from the single-host original.
30
+ */
31
+ export declare function qualifyGroupHeadingFor(row: QualifyGroupRow, state: QualifyGroupState): QualifyGroupHeading | null;
@@ -0,0 +1,23 @@
1
+ export interface ReviewDialogText {
2
+ title?: string;
3
+ /** Shown when the judge supplied no reason. The judge's reason always wins when present. */
4
+ fallbackReason?: string;
5
+ cost?: string;
6
+ decline?: string;
7
+ accept?: string;
8
+ }
9
+ export interface ReviewDialogOptions {
10
+ /** Overlay element id. Stable so tests and log-readers can find the same dialog anywhere. */
11
+ id?: string;
12
+ /** Host chrome. When given they are ADDED to the inline baseline, so a host theme (e.g. a
13
+ * dark-mode card background) can win where it should while the layout stays put. */
14
+ overlayClass?: string;
15
+ cardClass?: string;
16
+ /** The too-small-to-ask floor. Below it the dialog is a clipped smear over the chart, and
17
+ * asking illegibly for permission to spend money is worse than not asking. */
18
+ minWidth?: number;
19
+ minHeight?: number;
20
+ text?: ReviewDialogText;
21
+ }
22
+ /** Resolves true ONLY on a deliberate click of Apply. Never rejects. */
23
+ export declare function askApplyImprovements(container: HTMLElement, reason: string, opts?: ReviewDialogOptions): Promise<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.5.28",
3
+ "version": "0.5.30",
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",