@adoptai/genui-components 0.1.59 → 0.1.60

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.
@@ -283,8 +283,15 @@ function useContainerWidth(ref) {
283
283
  var KEYFRAMES = `
284
284
  @keyframes dcRise{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
285
285
  .dc-rise{animation:dcRise 0.28s cubic-bezier(0.22,1,0.36,1) both}
286
+ .dc-range{-webkit-appearance:none;appearance:none;width:100%;height:4px;border-radius:9999px;outline:none;cursor:pointer;background:transparent}
287
+ .dc-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;border-radius:50%;background:white;border:2px solid var(--dc-thumb,#ff5000);box-shadow:0 1px 3px rgba(0,0,0,0.18);cursor:grab;transition:transform 0.12s}
288
+ .dc-range::-webkit-slider-thumb:hover{transform:scale(1.12)}
289
+ .dc-range:active::-webkit-slider-thumb{cursor:grabbing}
290
+ .dc-range::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:white;border:2px solid var(--dc-thumb,#ff5000);box-shadow:0 1px 3px rgba(0,0,0,0.18);cursor:grab}
291
+ .dc-range::-moz-range-track{height:4px;border-radius:9999px;background:transparent}
286
292
  @media (prefers-reduced-motion: reduce){
287
293
  .dc-rise{animation:none !important;opacity:1 !important;transform:none !important}
294
+ .dc-range::-webkit-slider-thumb{transition:none}
288
295
  }`;
289
296
  function SparkCheck({ size, color }) {
290
297
  return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
@@ -330,17 +337,239 @@ function FlagPill({ flag }) {
330
337
  }
331
338
  );
332
339
  }
340
+ function formatFigure(value, unit) {
341
+ const body = Number.isInteger(value) ? `${value}` : `${Math.round(value * 100) / 100}`;
342
+ if (!unit) return body;
343
+ return unit === "%" ? `${body}%` : `${body} ${unit}`;
344
+ }
345
+ function clampToRange(raw, adjust) {
346
+ var _a;
347
+ const step = (_a = adjust.step) != null ? _a : Number.isInteger(adjust.min) && Number.isInteger(adjust.max) ? 1 : (adjust.max - adjust.min) / 100;
348
+ const snapped = Math.round((raw - adjust.min) / step) * step + adjust.min;
349
+ const fixed = Math.round(snapped * 1e6) / 1e6;
350
+ return Math.min(adjust.max, Math.max(adjust.min, fixed));
351
+ }
352
+ function ValueAdjuster({
353
+ option,
354
+ accent,
355
+ border,
356
+ muted,
357
+ onConfirm,
358
+ onBack
359
+ }) {
360
+ var _a, _b;
361
+ const adjust = option.adjust;
362
+ const suggested = clampToRange((_a = option.value) != null ? _a : adjust.min, adjust);
363
+ const [value, setValue] = React4.useState(suggested);
364
+ const [text, setText] = React4.useState(String(suggested));
365
+ const step = (_b = adjust.step) != null ? _b : Number.isInteger(adjust.min) && Number.isInteger(adjust.max) ? 1 : (adjust.max - adjust.min) / 100;
366
+ const pct = adjust.max > adjust.min ? (value - adjust.min) / (adjust.max - adjust.min) * 100 : 0;
367
+ const suggestedPct = adjust.max > adjust.min ? (suggested - adjust.min) / (adjust.max - adjust.min) * 100 : 0;
368
+ const moved = value !== suggested;
369
+ const commit = (raw) => {
370
+ const v = clampToRange(raw, adjust);
371
+ setValue(v);
372
+ setText(String(v));
373
+ };
374
+ return /* @__PURE__ */ jsxs(
375
+ "div",
376
+ {
377
+ className: "dc-rise",
378
+ "data-testid": "decision-card-adjuster",
379
+ style: {
380
+ display: "flex",
381
+ flexDirection: "column",
382
+ gap: 12,
383
+ border: `1px solid ${border}`,
384
+ borderRadius: 10,
385
+ padding: "12px 14px",
386
+ background: "#fafafa"
387
+ },
388
+ children: [
389
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 8 }, children: [
390
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.06em", color: muted }, children: [
391
+ "Adjust \xB7 ",
392
+ option.label
393
+ ] }),
394
+ /* @__PURE__ */ jsx("span", { style: { flex: 1 } }),
395
+ /* @__PURE__ */ jsx(
396
+ "button",
397
+ {
398
+ type: "button",
399
+ onClick: onBack,
400
+ "data-testid": "decision-card-adjust-back",
401
+ style: { border: "none", background: "none", padding: 0, cursor: "pointer", fontSize: 12, fontWeight: 600, color: muted },
402
+ children: "Back"
403
+ }
404
+ )
405
+ ] }),
406
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "baseline", gap: 6 }, children: [
407
+ /* @__PURE__ */ jsx(
408
+ "input",
409
+ {
410
+ value: text,
411
+ onChange: (e) => {
412
+ setText(e.target.value);
413
+ const n = Number(e.target.value);
414
+ if (Number.isFinite(n)) setValue(clampToRange(n, adjust));
415
+ },
416
+ onBlur: () => commit(Number(text) || suggested),
417
+ onKeyDown: (e) => {
418
+ if (e.key === "Enter") {
419
+ const v = clampToRange(Number(text) || suggested, adjust);
420
+ commit(v);
421
+ onConfirm(v);
422
+ } else if (e.key === "ArrowUp") {
423
+ e.preventDefault();
424
+ commit(value + step);
425
+ } else if (e.key === "ArrowDown") {
426
+ e.preventDefault();
427
+ commit(value - step);
428
+ }
429
+ },
430
+ inputMode: "decimal",
431
+ "aria-label": `${option.label} value`,
432
+ "data-testid": "decision-card-adjust-input",
433
+ style: {
434
+ width: `${Math.max(3, String(text).length + 1)}ch`,
435
+ fontSize: 26,
436
+ fontWeight: 700,
437
+ fontVariantNumeric: "tabular-nums",
438
+ letterSpacing: "-0.02em",
439
+ color: "var(--foreground)",
440
+ border: "none",
441
+ borderBottom: `2px solid ${accent}`,
442
+ background: "transparent",
443
+ outline: "none",
444
+ padding: 0
445
+ }
446
+ }
447
+ ),
448
+ adjust.unit && /* @__PURE__ */ jsx("span", { style: { fontSize: 13, fontWeight: 600, color: muted }, children: adjust.unit }),
449
+ moved && /* @__PURE__ */ jsxs(
450
+ "button",
451
+ {
452
+ type: "button",
453
+ onClick: () => commit(suggested),
454
+ "data-testid": "decision-card-adjust-reset",
455
+ style: {
456
+ marginLeft: 8,
457
+ border: "none",
458
+ background: "none",
459
+ padding: 0,
460
+ cursor: "pointer",
461
+ fontSize: 11.5,
462
+ fontWeight: 600,
463
+ color: muted,
464
+ textDecoration: "underline",
465
+ textUnderlineOffset: 2
466
+ },
467
+ children: [
468
+ "Reset to suggested (",
469
+ formatFigure(suggested, adjust.unit),
470
+ ")"
471
+ ]
472
+ }
473
+ )
474
+ ] }),
475
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [
476
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", height: 16, display: "flex", alignItems: "center" }, children: [
477
+ /* @__PURE__ */ jsx(
478
+ "div",
479
+ {
480
+ "aria-hidden": "true",
481
+ style: {
482
+ position: "absolute",
483
+ left: 0,
484
+ right: 0,
485
+ height: 4,
486
+ borderRadius: 9999,
487
+ background: `linear-gradient(to right, ${accent} ${pct}%, #e8e8e8 ${pct}%)`
488
+ }
489
+ }
490
+ ),
491
+ moved && /* @__PURE__ */ jsx(
492
+ "div",
493
+ {
494
+ "aria-hidden": "true",
495
+ title: `Suggested: ${formatFigure(suggested, adjust.unit)}`,
496
+ style: {
497
+ position: "absolute",
498
+ left: `${suggestedPct}%`,
499
+ transform: "translateX(-50%)",
500
+ width: 2,
501
+ height: 10,
502
+ borderRadius: 1,
503
+ background: muted,
504
+ opacity: 0.55
505
+ }
506
+ }
507
+ ),
508
+ /* @__PURE__ */ jsx(
509
+ "input",
510
+ {
511
+ type: "range",
512
+ className: "dc-range",
513
+ min: adjust.min,
514
+ max: adjust.max,
515
+ step,
516
+ value,
517
+ onChange: (e) => commit(Number(e.target.value)),
518
+ "aria-label": `${option.label} slider`,
519
+ "data-testid": "decision-card-adjust-slider",
520
+ style: { position: "relative", ["--dc-thumb"]: accent }
521
+ }
522
+ )
523
+ ] }),
524
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", justifyContent: "space-between", fontSize: 10.5, fontWeight: 600, color: muted, fontVariantNumeric: "tabular-nums" }, children: [
525
+ /* @__PURE__ */ jsx("span", { children: formatFigure(adjust.min, adjust.unit) }),
526
+ /* @__PURE__ */ jsx("span", { children: formatFigure(adjust.max, adjust.unit) })
527
+ ] })
528
+ ] }),
529
+ /* @__PURE__ */ jsxs(
530
+ "button",
531
+ {
532
+ type: "button",
533
+ onClick: () => onConfirm(value),
534
+ "data-testid": "decision-card-adjust-confirm",
535
+ style: {
536
+ alignSelf: "flex-start",
537
+ border: `1px solid ${accent}`,
538
+ background: accent,
539
+ color: "white",
540
+ borderRadius: 8,
541
+ fontSize: 13,
542
+ fontWeight: 600,
543
+ padding: "9px 16px",
544
+ cursor: "pointer",
545
+ fontVariantNumeric: "tabular-nums",
546
+ transition: "opacity 0.15s"
547
+ },
548
+ onMouseEnter: (e) => e.currentTarget.style.opacity = "0.88",
549
+ onMouseLeave: (e) => e.currentTarget.style.opacity = "1",
550
+ children: [
551
+ option.label,
552
+ " \xB7 ",
553
+ formatFigure(value, adjust.unit)
554
+ ]
555
+ }
556
+ )
557
+ ]
558
+ }
559
+ );
560
+ }
333
561
  function DecisionCardRenderer({
334
562
  data,
335
563
  resolved = null,
336
564
  onResolve,
337
565
  onOpenSource
338
566
  }) {
339
- var _a, _b;
567
+ var _a, _b, _c;
340
568
  const { BORDER: BORDER2, MUTED: MUTED2, ACCENT: ACCENT2, ACCENT_SOFT: ACCENT_SOFT2 } = useTheme();
341
569
  const rootRef = React4.useRef(null);
342
570
  const width = useContainerWidth(rootRef);
343
571
  const stacked = width > 0 && width < STACK_BELOW;
572
+ const [adjustingIdx, setAdjustingIdx] = React4.useState(null);
344
573
  const options = (_a = data.options) != null ? _a : [];
345
574
  const recommendedIdx = Math.max(
346
575
  0,
@@ -495,7 +724,20 @@ function DecisionCardRenderer({
495
724
  ]
496
725
  }
497
726
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
498
- /* @__PURE__ */ jsx(
727
+ adjustingIdx != null && ((_b = options[adjustingIdx]) == null ? void 0 : _b.adjust) ? /* @__PURE__ */ jsx(
728
+ ValueAdjuster,
729
+ {
730
+ option: options[adjustingIdx],
731
+ accent: ACCENT2,
732
+ border: BORDER2,
733
+ muted: MUTED2,
734
+ onBack: () => setAdjustingIdx(null),
735
+ onConfirm: (value) => {
736
+ setAdjustingIdx(null);
737
+ onResolve == null ? void 0 : onResolve(options[adjustingIdx].label, value);
738
+ }
739
+ }
740
+ ) : /* @__PURE__ */ jsx(
499
741
  "div",
500
742
  {
501
743
  style: {
@@ -505,15 +747,21 @@ function DecisionCardRenderer({
505
747
  gap: 8
506
748
  },
507
749
  children: options.map((opt, idx) => {
508
- var _a2;
750
+ var _a2, _b2;
509
751
  const primary = idx === recommendedIdx;
510
- return /* @__PURE__ */ jsx(
752
+ const adjustable = Boolean(opt.adjust);
753
+ return /* @__PURE__ */ jsxs(
511
754
  "button",
512
755
  {
513
756
  type: "button",
514
- onClick: () => onResolve == null ? void 0 : onResolve(opt.label),
757
+ onClick: () => adjustable ? setAdjustingIdx(idx) : onResolve == null ? void 0 : onResolve(opt.label),
758
+ "data-adjustable": adjustable || void 0,
515
759
  style: {
516
760
  width: stacked ? "100%" : "auto",
761
+ display: "inline-flex",
762
+ alignItems: "center",
763
+ justifyContent: stacked ? "flex-start" : "center",
764
+ gap: 7,
517
765
  textAlign: stacked ? "left" : "center",
518
766
  border: `1px solid ${primary ? ACCENT2 : BORDER2}`,
519
767
  background: primary ? ACCENT2 : "white",
@@ -541,7 +789,28 @@ function DecisionCardRenderer({
541
789
  e.currentTarget.style.background = "white";
542
790
  }
543
791
  },
544
- children: opt.label
792
+ children: [
793
+ opt.label,
794
+ adjustable && typeof opt.value === "number" && /* @__PURE__ */ jsxs(
795
+ "span",
796
+ {
797
+ style: {
798
+ fontSize: 11.5,
799
+ fontWeight: 700,
800
+ fontVariantNumeric: "tabular-nums",
801
+ padding: "1px 7px",
802
+ borderRadius: 9999,
803
+ background: primary ? "rgba(255,255,255,0.22)" : "#f2f2f2",
804
+ color: primary ? "white" : MUTED2,
805
+ lineHeight: 1.5
806
+ },
807
+ children: [
808
+ formatFigure(opt.value, (_b2 = opt.adjust) == null ? void 0 : _b2.unit),
809
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { marginLeft: 4, opacity: 0.8 }, children: "\u270E" })
810
+ ]
811
+ }
812
+ )
813
+ ]
545
814
  },
546
815
  (_a2 = opt.id) != null ? _a2 : opt.label
547
816
  );
@@ -580,7 +849,7 @@ function DecisionCardRenderer({
580
849
  strokeLinejoin: "round"
581
850
  }
582
851
  ) }),
583
- (_b = data.source.label) != null ? _b : "View source for this figure"
852
+ (_c = data.source.label) != null ? _c : "View source for this figure"
584
853
  ]
585
854
  }
586
855
  )
@@ -589,20 +858,27 @@ function DecisionCardRenderer({
589
858
  }
590
859
  );
591
860
  }
861
+ function composeResolution(p, option, value) {
862
+ var _a, _b, _c;
863
+ if (value == null) return option;
864
+ const unit = (_c = (_b = ((_a = p.options) != null ? _a : []).find((o) => o.label === option)) == null ? void 0 : _b.adjust) == null ? void 0 : _c.unit;
865
+ return `${option} \xB7 ${formatFigure(value, unit)}`;
866
+ }
592
867
  function DecisionCardResolver(p) {
593
- var _a, _b, _c, _d;
868
+ var _a, _b;
594
869
  const { resolvedDecisions, onDecisionResolve, onDecisionSource } = useGenUIInteraction();
595
870
  const decisionId = (_a = p.id) != null ? _a : p.title;
596
871
  const hostResolved = resolvedDecisions == null ? void 0 : resolvedDecisions[decisionId];
597
- const initial = (_c = hostResolved != null ? hostResolved : (_b = p.resolved) == null ? void 0 : _b.option) != null ? _c : null;
872
+ const initial = hostResolved != null ? hostResolved : p.resolved ? composeResolution(p, p.resolved.option, p.resolved.value) : null;
598
873
  const [localResolved, setLocalResolved] = React4.useState(initial);
599
874
  React4.useEffect(() => {
600
875
  if (hostResolved !== void 0) setLocalResolved(hostResolved);
601
876
  }, [hostResolved]);
602
877
  const resolved = hostResolved != null ? hostResolved : localResolved;
603
- const handleResolve = (option) => {
604
- setLocalResolved(option);
605
- onDecisionResolve == null ? void 0 : onDecisionResolve(decisionId, option);
878
+ const handleResolve = (option, value) => {
879
+ const resolution = composeResolution(p, option, value);
880
+ setLocalResolved(resolution);
881
+ onDecisionResolve == null ? void 0 : onDecisionResolve(decisionId, resolution);
606
882
  };
607
883
  const handleSource = () => {
608
884
  var _a2;
@@ -611,7 +887,7 @@ function DecisionCardResolver(p) {
611
887
  window.open(p.source.url, "_blank", "noopener,noreferrer");
612
888
  }
613
889
  };
614
- return /* @__PURE__ */ jsx(ComponentActions, { filename: (_d = p.title) != null ? _d : "decision-card", children: /* @__PURE__ */ jsx(
890
+ return /* @__PURE__ */ jsx(ComponentActions, { filename: (_b = p.title) != null ? _b : "decision-card", children: /* @__PURE__ */ jsx(
615
891
  DecisionCardRenderer,
616
892
  {
617
893
  data: p,