@tensor_1/react 2.12.0 → 2.13.0

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.
Files changed (42) hide show
  1. package/README.md +16 -0
  2. package/dist/calendar-model.d.ts +31 -0
  3. package/dist/calendar-model.js +89 -0
  4. package/dist/color-model.d.ts +20 -0
  5. package/dist/color-model.js +74 -0
  6. package/dist/components/BrandProfiles.d.ts +45 -0
  7. package/dist/components/BrandProfiles.js +150 -0
  8. package/dist/components/BuilderCanvas.d.ts +34 -0
  9. package/dist/components/BuilderCanvas.js +117 -0
  10. package/dist/components/ColorPicker.d.ts +17 -0
  11. package/dist/components/ColorPicker.js +158 -0
  12. package/dist/components/DateRangePicker.d.ts +23 -0
  13. package/dist/components/DateRangePicker.js +123 -0
  14. package/dist/components/DiffViewer.d.ts +18 -0
  15. package/dist/components/DiffViewer.js +93 -0
  16. package/dist/components/LicensePositionBar.d.ts +17 -0
  17. package/dist/components/LicensePositionBar.js +50 -0
  18. package/dist/components/MatchReview.d.ts +39 -0
  19. package/dist/components/MatchReview.js +120 -0
  20. package/dist/components/MetricInput.d.ts +23 -0
  21. package/dist/components/MetricInput.js +92 -0
  22. package/dist/components/RelationshipGraph.d.ts +30 -0
  23. package/dist/components/RelationshipGraph.js +138 -0
  24. package/dist/components/RuleBuilder.d.ts +45 -0
  25. package/dist/components/RuleBuilder.js +163 -0
  26. package/dist/components/SchedulingCalendar.d.ts +42 -0
  27. package/dist/components/SchedulingCalendar.js +124 -0
  28. package/dist/diff-model.d.ts +23 -0
  29. package/dist/diff-model.js +50 -0
  30. package/dist/index.cjs +1753 -2
  31. package/dist/index.d.cts +439 -1
  32. package/dist/index.d.ts +18 -0
  33. package/dist/index.js +91 -1
  34. package/dist/json-value.d.ts +10 -0
  35. package/dist/json-value.js +19 -0
  36. package/dist/metric-model.d.ts +9 -0
  37. package/dist/metric-model.js +25 -0
  38. package/dist/relationship-graph-model.d.ts +31 -0
  39. package/dist/relationship-graph-model.js +68 -0
  40. package/dist/rule-builder-model.d.ts +41 -0
  41. package/dist/rule-builder-model.js +101 -0
  42. package/package.json +1 -1
@@ -0,0 +1,158 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { contrastRatio, hexToRgb, hsvToHex, normalizeHex, rgbToHsv } from "../color-model.js";
6
+ const EMPTY_COLORS = [];
7
+ function ColorPicker({
8
+ value,
9
+ onChange,
10
+ foreground,
11
+ presets = EMPTY_COLORS,
12
+ recent = EMPTY_COLORS,
13
+ minimumContrast = 4.5,
14
+ label = "Choose color",
15
+ disabled,
16
+ className
17
+ }) {
18
+ const id = useId();
19
+ const triggerRef = useRef(null);
20
+ const dialogRef = useRef(null);
21
+ const [open, setOpen] = useState(false);
22
+ const normalized = normalizeHex(value) ?? "#000000";
23
+ const [hexText, setHexText] = useState(normalized);
24
+ const hsv = useMemo(() => rgbToHsv(hexToRgb(normalized)), [normalized]);
25
+ const ratio = contrastRatio(normalized, foreground) ?? 0;
26
+ const passes = ratio >= minimumContrast;
27
+ useEffect(() => setHexText(normalized), [normalized]);
28
+ useEffect(() => {
29
+ if (!open) return;
30
+ const dialog = dialogRef.current;
31
+ dialog?.querySelector("input,button")?.focus();
32
+ const onPointer = (event) => {
33
+ if (!dialog?.contains(event.target) && !triggerRef.current?.contains(event.target)) setOpen(false);
34
+ };
35
+ document.addEventListener("mousedown", onPointer);
36
+ return () => document.removeEventListener("mousedown", onPointer);
37
+ }, [open]);
38
+ const close = () => {
39
+ setOpen(false);
40
+ requestAnimationFrame(() => triggerRef.current?.focus());
41
+ };
42
+ const onDialogKeyDown = (event) => {
43
+ if (event.key === "Escape") {
44
+ event.preventDefault();
45
+ close();
46
+ return;
47
+ }
48
+ if (event.key !== "Tab") return;
49
+ const focusable = Array.from(dialogRef.current?.querySelectorAll('button:not([disabled]),input:not([disabled]),[tabindex="0"]') ?? []);
50
+ if (!focusable.length) return;
51
+ const first = focusable[0];
52
+ const last = focusable[focusable.length - 1];
53
+ if (event.shiftKey && document.activeElement === first) {
54
+ event.preventDefault();
55
+ last.focus();
56
+ } else if (!event.shiftKey && document.activeElement === last) {
57
+ event.preventDefault();
58
+ first.focus();
59
+ }
60
+ };
61
+ const updateHsv = (part) => onChange(hsvToHex({ ...hsv, ...part }));
62
+ const commitHex = () => {
63
+ const next = normalizeHex(hexText);
64
+ if (next) onChange(next);
65
+ };
66
+ const choose = (candidate) => {
67
+ const next = normalizeHex(candidate);
68
+ if (next) onChange(next);
69
+ };
70
+ const sampleStyle = { backgroundColor: normalized };
71
+ return /* @__PURE__ */ jsxs("div", { className: cx("uix-color-picker", className), children: [
72
+ /* @__PURE__ */ jsxs(
73
+ "button",
74
+ {
75
+ ref: triggerRef,
76
+ type: "button",
77
+ className: "uix-color-picker__trigger",
78
+ "aria-label": `${label}: ${normalized}`,
79
+ "aria-expanded": open,
80
+ "aria-controls": `${id}-dialog`,
81
+ onClick: () => setOpen((current) => !current),
82
+ disabled,
83
+ children: [
84
+ /* @__PURE__ */ jsx("span", { className: "uix-color-picker__swatch", style: sampleStyle, "aria-hidden": "true" }),
85
+ /* @__PURE__ */ jsx("span", { children: normalized })
86
+ ]
87
+ }
88
+ ),
89
+ open && /* @__PURE__ */ jsxs(
90
+ "div",
91
+ {
92
+ ref: dialogRef,
93
+ id: `${id}-dialog`,
94
+ role: "dialog",
95
+ "aria-modal": "true",
96
+ "aria-label": label,
97
+ className: "uix-color-picker__popover",
98
+ onKeyDown: onDialogKeyDown,
99
+ children: [
100
+ /* @__PURE__ */ jsx("div", { className: "uix-color-picker__preview", style: sampleStyle, children: /* @__PURE__ */ jsx("span", { style: { color: foreground }, children: "Aa" }) }),
101
+ /* @__PURE__ */ jsxs("label", { className: "uix-color-picker__range", children: [
102
+ "Hue ",
103
+ /* @__PURE__ */ jsxs("span", { children: [
104
+ Math.round(hsv.h),
105
+ "\xB0"
106
+ ] }),
107
+ /* @__PURE__ */ jsx("input", { type: "range", min: "0", max: "359", value: Math.round(hsv.h), onChange: (event) => updateHsv({ h: Number(event.currentTarget.value) }) })
108
+ ] }),
109
+ /* @__PURE__ */ jsxs("label", { className: "uix-color-picker__range", children: [
110
+ "Saturation ",
111
+ /* @__PURE__ */ jsxs("span", { children: [
112
+ Math.round(hsv.s * 100),
113
+ "%"
114
+ ] }),
115
+ /* @__PURE__ */ jsx("input", { type: "range", min: "0", max: "100", value: Math.round(hsv.s * 100), onChange: (event) => updateHsv({ s: Number(event.currentTarget.value) / 100 }) })
116
+ ] }),
117
+ /* @__PURE__ */ jsxs("label", { className: "uix-color-picker__range", children: [
118
+ "Brightness ",
119
+ /* @__PURE__ */ jsxs("span", { children: [
120
+ Math.round(hsv.v * 100),
121
+ "%"
122
+ ] }),
123
+ /* @__PURE__ */ jsx("input", { type: "range", min: "0", max: "100", value: Math.round(hsv.v * 100), onChange: (event) => updateHsv({ v: Number(event.currentTarget.value) / 100 }) })
124
+ ] }),
125
+ /* @__PURE__ */ jsxs("label", { className: "uix-color-picker__hex", children: [
126
+ "Hex",
127
+ /* @__PURE__ */ jsxs("span", { className: "uix-color-picker__hex-row", children: [
128
+ /* @__PURE__ */ jsx("input", { className: "uix-input", value: hexText, onChange: (event) => setHexText(event.currentTarget.value), onBlur: commitHex, onKeyDown: (event) => {
129
+ if (event.key === "Enter") commitHex();
130
+ }, "aria-invalid": !normalizeHex(hexText) }),
131
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary uix-btn--sm", onClick: commitHex, disabled: !normalizeHex(hexText), children: "Set" })
132
+ ] })
133
+ ] }),
134
+ (presets.length > 0 || recent.length > 0) && /* @__PURE__ */ jsxs("div", { className: "uix-color-picker__palettes", children: [
135
+ presets.length > 0 && /* @__PURE__ */ jsxs("fieldset", { children: [
136
+ /* @__PURE__ */ jsx("legend", { children: "Presets" }),
137
+ /* @__PURE__ */ jsx("div", { children: presets.map((color) => /* @__PURE__ */ jsx("button", { type: "button", className: "uix-color-picker__preset", style: { backgroundColor: normalizeHex(color) ?? color }, onClick: () => choose(color), "aria-label": `Use preset ${color}` }, color)) })
138
+ ] }),
139
+ recent.length > 0 && /* @__PURE__ */ jsxs("fieldset", { children: [
140
+ /* @__PURE__ */ jsx("legend", { children: "Recent" }),
141
+ /* @__PURE__ */ jsx("div", { children: recent.map((color) => /* @__PURE__ */ jsx("button", { type: "button", className: "uix-color-picker__preset", style: { backgroundColor: normalizeHex(color) ?? color }, onClick: () => choose(color), "aria-label": `Use recent color ${color}` }, color)) })
142
+ ] })
143
+ ] }),
144
+ /* @__PURE__ */ jsxs("p", { className: cx("uix-color-picker__contrast", passes ? "uix-color-picker__contrast--pass" : "uix-color-picker__contrast--fail"), role: "status", "aria-live": "polite", children: [
145
+ "Contrast ",
146
+ ratio.toFixed(2),
147
+ ":1 \u2014 ",
148
+ passes ? `passes WCAG AA (${minimumContrast}:1)` : `does not meet WCAG AA (${minimumContrast}:1)`
149
+ ] }),
150
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--primary uix-btn--sm", onClick: close, children: "Done" })
151
+ ]
152
+ }
153
+ )
154
+ ] });
155
+ }
156
+ export {
157
+ ColorPicker
158
+ };
@@ -0,0 +1,23 @@
1
+ import * as react from 'react';
2
+ import { DateRangeValue } from '../calendar-model.js';
3
+
4
+ interface DateRangePickerProps {
5
+ value: DateRangeValue;
6
+ onChange: (value: DateRangeValue) => void;
7
+ visibleMonth?: string;
8
+ onVisibleMonthChange?: (month: string) => void;
9
+ months?: number;
10
+ min?: string;
11
+ max?: string;
12
+ isDateDisabled?: (date: string) => boolean;
13
+ locale?: string;
14
+ label?: string;
15
+ invalid?: boolean;
16
+ invalidMessage?: string;
17
+ disabled?: boolean;
18
+ className?: string;
19
+ }
20
+ /** Controlled, two-month range picker using the UIx calendar language. */
21
+ declare function DateRangePicker({ value, onChange, visibleMonth: controlledMonth, onVisibleMonthChange, months, min, max, isDateDisabled, locale, label, invalid, invalidMessage, disabled, className, }: DateRangePickerProps): react.JSX.Element;
22
+
23
+ export { DateRangePicker, type DateRangePickerProps };
@@ -0,0 +1,123 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
4
+ import { addCalendarDays, addCalendarMonths, buildMonthGrid, isDateInRange, isDateUnavailable, selectRangeDate, startOfMonth } from "../calendar-model.js";
5
+ import { cx } from "../cx.js";
6
+ const monthLabel = (date, locale) => new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
7
+ const dateLabel = (date, locale) => new Intl.DateTimeFormat(locale, { dateStyle: "full", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
8
+ function DateRangePicker({
9
+ value,
10
+ onChange,
11
+ visibleMonth: controlledMonth,
12
+ onVisibleMonthChange,
13
+ months = 2,
14
+ min,
15
+ max,
16
+ isDateDisabled,
17
+ locale,
18
+ label = "Choose date range",
19
+ invalid,
20
+ invalidMessage = "The selected date range is invalid.",
21
+ disabled,
22
+ className
23
+ }) {
24
+ const id = useId();
25
+ const [internalMonth, setInternalMonth] = useState(() => startOfMonth(controlledMonth ?? value.start ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)));
26
+ const visibleMonth = startOfMonth(controlledMonth ?? internalMonth);
27
+ const [activeDate, setActiveDate] = useState(value.end ?? value.start ?? visibleMonth);
28
+ const rootRef = useRef(null);
29
+ const pendingFocus = useRef(false);
30
+ const count = Math.max(1, Math.min(3, months));
31
+ const calendars = useMemo(() => Array.from({ length: count }, (_, index) => {
32
+ const month = startOfMonth(addCalendarMonths(visibleMonth, index));
33
+ return { month, days: buildMonthGrid(month).filter((day) => day.inMonth) };
34
+ }), [visibleMonth, count]);
35
+ const rangeInvalid = invalid || !!value.start && !!value.end && value.start > value.end;
36
+ useEffect(() => {
37
+ const first = calendars[0]?.days[0]?.date;
38
+ const finalDays = calendars[calendars.length - 1]?.days;
39
+ const last = finalDays?.[finalDays.length - 1]?.date;
40
+ if (first && last && (activeDate < first || activeDate > last)) setActiveDate(first);
41
+ }, [activeDate, calendars]);
42
+ useEffect(() => {
43
+ if (!pendingFocus.current) return;
44
+ rootRef.current?.querySelector(`[data-date="${activeDate}"]`)?.focus();
45
+ pendingFocus.current = false;
46
+ }, [activeDate, visibleMonth]);
47
+ const setMonth = (month) => {
48
+ const next = startOfMonth(month);
49
+ if (controlledMonth === void 0) setInternalMonth(next);
50
+ onVisibleMonthChange?.(next);
51
+ };
52
+ const focusDate = (date) => {
53
+ setActiveDate(date);
54
+ if (date < calendars[0].month || date.slice(0, 7) > calendars[calendars.length - 1].month.slice(0, 7)) setMonth(startOfMonth(date));
55
+ pendingFocus.current = true;
56
+ };
57
+ const choose = (date) => {
58
+ if (disabled || isDateUnavailable(date, { min, max, isDisabled: isDateDisabled })) return;
59
+ onChange(selectRangeDate(value, date));
60
+ setActiveDate(date);
61
+ };
62
+ const onDateKeyDown = (event, date) => {
63
+ const offsets = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
64
+ if (event.key in offsets) {
65
+ event.preventDefault();
66
+ focusDate(addCalendarDays(date, offsets[event.key]));
67
+ return;
68
+ }
69
+ if (event.key === "Home" || event.key === "End") {
70
+ event.preventDefault();
71
+ const weekday = (/* @__PURE__ */ new Date(`${date}T00:00:00Z`)).getUTCDay();
72
+ const mondayIndex = (weekday + 6) % 7;
73
+ focusDate(addCalendarDays(date, event.key === "Home" ? -mondayIndex : 6 - mondayIndex));
74
+ } else if (event.key === "PageUp" || event.key === "PageDown") {
75
+ event.preventDefault();
76
+ focusDate(addCalendarMonths(date, event.key === "PageUp" ? -1 : 1));
77
+ }
78
+ };
79
+ const announcement = !value.start ? "No date range selected." : !value.end ? `Start date ${dateLabel(value.start, locale)} selected. Choose an end date.` : `${dateLabel(value.start, locale)} to ${dateLabel(value.end, locale)} selected.`;
80
+ return /* @__PURE__ */ jsxs("section", { className: cx("uix-date-range-picker", rangeInvalid && "uix-date-range-picker--invalid", className), "aria-labelledby": `${id}-label`, children: [
81
+ /* @__PURE__ */ jsxs("div", { className: "uix-date-range-picker__header", children: [
82
+ /* @__PURE__ */ jsx("h3", { id: `${id}-label`, children: label }),
83
+ /* @__PURE__ */ jsxs("div", { children: [
84
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: () => setMonth(addCalendarMonths(visibleMonth, -1)), "aria-label": "Show previous month", children: "Previous" }),
85
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: () => setMonth(addCalendarMonths(visibleMonth, 1)), "aria-label": "Show next month", children: "Next" })
86
+ ] })
87
+ ] }),
88
+ /* @__PURE__ */ jsx("div", { ref: rootRef, className: "uix-date-range-picker__months", children: calendars.map(({ month, days }) => /* @__PURE__ */ jsxs("div", { className: "uix-date-range-picker__month", children: [
89
+ /* @__PURE__ */ jsx("h4", { children: monthLabel(month, locale) }),
90
+ /* @__PURE__ */ jsx("div", { className: "uix-date-range-picker__weekdays", "aria-hidden": "true", children: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((day) => /* @__PURE__ */ jsx("span", { children: day }, day)) }),
91
+ /* @__PURE__ */ jsxs("div", { className: "uix-date-range-picker__grid", role: "group", "aria-label": monthLabel(month, locale), children: [
92
+ Array.from({ length: ((/* @__PURE__ */ new Date(`${month}T00:00:00Z`)).getUTCDay() + 6) % 7 }, (_, index) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true" }, `blank-${index}`)),
93
+ days.map((day) => {
94
+ const unavailable = disabled || isDateUnavailable(day.date, { min, max, isDisabled: isDateDisabled });
95
+ const edge = day.date === value.start ? "start" : day.date === value.end ? "end" : void 0;
96
+ return /* @__PURE__ */ jsx(
97
+ "button",
98
+ {
99
+ type: "button",
100
+ "data-date": day.date,
101
+ "data-range-edge": edge,
102
+ "data-in-range": isDateInRange(day.date, value) || void 0,
103
+ "aria-pressed": edge != null || isDateInRange(day.date, value),
104
+ "aria-label": `${dateLabel(day.date, locale)}${edge ? `, ${edge} of selected range` : isDateInRange(day.date, value) ? ", in selected range" : ""}`,
105
+ tabIndex: activeDate === day.date ? 0 : -1,
106
+ disabled: unavailable,
107
+ onFocus: () => setActiveDate(day.date),
108
+ onKeyDown: (event) => onDateKeyDown(event, day.date),
109
+ onClick: () => choose(day.date),
110
+ children: day.day
111
+ },
112
+ day.date
113
+ );
114
+ })
115
+ ] })
116
+ ] }, month)) }),
117
+ /* @__PURE__ */ jsx("p", { className: "uix-date-range-picker__selection", "aria-live": "polite", children: announcement }),
118
+ rangeInvalid && /* @__PURE__ */ jsx("p", { className: "uix-field__error", role: "alert", children: invalidMessage })
119
+ ] });
120
+ }
121
+ export {
122
+ DateRangePicker
123
+ };
@@ -0,0 +1,18 @@
1
+ import * as react from 'react';
2
+ import { JsonValue } from '../json-value.js';
3
+ import { DiffEntry, DiffResolution } from '../diff-model.js';
4
+
5
+ interface DiffViewerProps {
6
+ base?: JsonValue;
7
+ current?: JsonValue;
8
+ incoming?: JsonValue;
9
+ entries?: DiffEntry[];
10
+ resolutions?: Record<string, DiffResolution>;
11
+ onResolutionChange?: (path: string, resolution: DiffResolution) => void;
12
+ labels?: Partial<Record<'base' | 'current' | 'incoming', string>>;
13
+ className?: string;
14
+ }
15
+ /** Three-way configuration diff with per-entry keyboard-operable resolution. */
16
+ declare function DiffViewer({ base, current, incoming, entries: providedEntries, resolutions: controlledResolutions, onResolutionChange, labels, className, }: DiffViewerProps): react.JSX.Element;
17
+
18
+ export { DiffViewer, type DiffViewerProps };
@@ -0,0 +1,93 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId, useMemo, useState } from "react";
4
+ import { buildThreeWayDiff, summarizeDiff } from "../diff-model.js";
5
+ import { cx } from "../cx.js";
6
+ import { StatusPill } from "./StatusPill.js";
7
+ const GROUPS = [
8
+ { kind: "conflicted", label: "Conflicted" },
9
+ { kind: "changed", label: "Changed" },
10
+ { kind: "added", label: "Added" },
11
+ { kind: "removed", label: "Removed" }
12
+ ];
13
+ const DEFAULT_LABELS = {};
14
+ const renderValue = (value) => value === void 0 ? "Not present" : JSON.stringify(value, null, 2);
15
+ function DiffViewer({
16
+ base,
17
+ current,
18
+ incoming,
19
+ entries: providedEntries,
20
+ resolutions: controlledResolutions,
21
+ onResolutionChange,
22
+ labels = DEFAULT_LABELS,
23
+ className
24
+ }) {
25
+ const id = useId();
26
+ const entries = useMemo(() => providedEntries ?? buildThreeWayDiff(base, current, incoming), [providedEntries, base, current, incoming]);
27
+ const [internalResolutions, setInternalResolutions] = useState({});
28
+ const resolutions = controlledResolutions ?? internalResolutions;
29
+ const summary = summarizeDiff(entries, resolutions);
30
+ const resolve = (path, resolution) => {
31
+ if (controlledResolutions === void 0) setInternalResolutions((currentState) => ({ ...currentState, [path]: resolution }));
32
+ onResolutionChange?.(path, resolution);
33
+ };
34
+ if (entries.length === 0) return /* @__PURE__ */ jsxs("section", { className: cx("uix-diff-viewer uix-diff-viewer--empty", className), "aria-label": "Configuration differences", children: [
35
+ /* @__PURE__ */ jsx("h3", { children: "No differences" }),
36
+ /* @__PURE__ */ jsx("p", { children: "Base, current, and incoming values are aligned." })
37
+ ] });
38
+ return /* @__PURE__ */ jsxs("section", { className: cx("uix-diff-viewer", className), "aria-label": "Configuration differences", children: [
39
+ /* @__PURE__ */ jsxs("div", { className: "uix-diff-viewer__summary", "aria-label": "Difference summary", children: [
40
+ GROUPS.map((group) => /* @__PURE__ */ jsxs("span", { children: [
41
+ /* @__PURE__ */ jsx("b", { children: summary[group.kind] }),
42
+ " ",
43
+ group.label.toLowerCase()
44
+ ] }, group.kind)),
45
+ /* @__PURE__ */ jsxs("span", { children: [
46
+ /* @__PURE__ */ jsx("b", { children: summary.resolved }),
47
+ " resolved"
48
+ ] }),
49
+ /* @__PURE__ */ jsxs("span", { children: [
50
+ /* @__PURE__ */ jsx("b", { children: summary.pending }),
51
+ " pending"
52
+ ] })
53
+ ] }),
54
+ GROUPS.map((group) => {
55
+ const grouped = entries.filter((entry) => entry.kind === group.kind);
56
+ if (!grouped.length) return null;
57
+ return /* @__PURE__ */ jsxs("section", { className: "uix-diff-viewer__group", "aria-labelledby": `${id}-${group.kind}`, children: [
58
+ /* @__PURE__ */ jsxs("h3", { id: `${id}-${group.kind}`, children: [
59
+ group.label,
60
+ " ",
61
+ /* @__PURE__ */ jsx("span", { children: grouped.length })
62
+ ] }),
63
+ grouped.map((entry) => {
64
+ const resolution = resolutions[entry.path] ?? "pending";
65
+ return /* @__PURE__ */ jsxs("details", { className: "uix-diff-viewer__entry", "data-kind": entry.kind, open: entry.kind === "conflicted", children: [
66
+ /* @__PURE__ */ jsxs("summary", { children: [
67
+ /* @__PURE__ */ jsx("code", { children: entry.path }),
68
+ /* @__PURE__ */ jsx(StatusPill, { tone: resolution === "pending" ? "warning" : "success", children: resolution })
69
+ ] }),
70
+ /* @__PURE__ */ jsx("div", { className: "uix-diff-viewer__versions", children: ["base", "current", "incoming"].map((version) => /* @__PURE__ */ jsxs("div", { role: "region", "aria-label": `${labels[version] ?? `${version[0].toUpperCase()}${version.slice(1)}`} value for ${entry.path}`, children: [
71
+ /* @__PURE__ */ jsx("strong", { children: labels[version] ?? `${version[0].toUpperCase()}${version.slice(1)}` }),
72
+ /* @__PURE__ */ jsx("pre", { children: renderValue(entry[version]) })
73
+ ] }, version)) }),
74
+ /* @__PURE__ */ jsxs("div", { className: "uix-diff-viewer__actions", "aria-label": `Resolve ${entry.path}`, children: [
75
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--primary uix-btn--sm", "aria-pressed": resolution === "accept", onClick: () => resolve(entry.path, "accept"), children: "Accept incoming" }),
76
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary uix-btn--sm", "aria-pressed": resolution === "skip", onClick: () => resolve(entry.path, "skip"), children: "Keep current" }),
77
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", "aria-pressed": resolution === "pending", onClick: () => resolve(entry.path, "pending"), children: "Mark pending" })
78
+ ] })
79
+ ] }, entry.path);
80
+ })
81
+ ] }, group.kind);
82
+ }),
83
+ /* @__PURE__ */ jsxs("p", { className: "uix-visually-hidden", "aria-live": "polite", children: [
84
+ summary.resolved,
85
+ " of ",
86
+ entries.length,
87
+ " differences resolved."
88
+ ] })
89
+ ] });
90
+ }
91
+ export {
92
+ DiffViewer
93
+ };
@@ -0,0 +1,17 @@
1
+ import * as react from 'react';
2
+ import { HTMLAttributes } from 'react';
3
+
4
+ interface LicensePositionBarProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
5
+ consumed: number;
6
+ entitled: number;
7
+ consumedLabel?: string;
8
+ entitledLabel?: string;
9
+ availableLabel?: string;
10
+ overLabel?: string;
11
+ unit?: string;
12
+ formatValue?: (value: number) => string;
13
+ }
14
+ /** Capacity-versus-limit recipe with a visible and textual over-allocation state. */
15
+ declare function LicensePositionBar({ consumed, entitled, consumedLabel, entitledLabel, availableLabel, overLabel, unit, formatValue, className, ...props }: LicensePositionBarProps): react.JSX.Element;
16
+
17
+ export { LicensePositionBar, type LicensePositionBarProps };
@@ -0,0 +1,50 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { cx } from "../cx.js";
3
+ import { Meter } from "./Meter.js";
4
+ import { StatusPill } from "./StatusPill.js";
5
+ const defaultCapacityFormat = (value) => new Intl.NumberFormat().format(value);
6
+ function LicensePositionBar({
7
+ consumed,
8
+ entitled,
9
+ consumedLabel = "Consumed",
10
+ entitledLabel = "Entitled",
11
+ availableLabel = "available",
12
+ overLabel = "over limit",
13
+ unit = "",
14
+ formatValue = defaultCapacityFormat,
15
+ className,
16
+ ...props
17
+ }) {
18
+ const safeConsumed = Math.max(0, consumed);
19
+ const safeEntitled = Math.max(0, entitled);
20
+ const over = Math.max(0, safeConsumed - safeEntitled);
21
+ const available = Math.max(0, safeEntitled - safeConsumed);
22
+ const percent = safeEntitled === 0 ? safeConsumed > 0 ? 100 : 0 : safeConsumed / safeEntitled * 100;
23
+ const format = (value) => `${formatValue(value)}${unit ? ` ${unit}` : ""}`;
24
+ const summary = `${consumedLabel}: ${format(safeConsumed)}. ${entitledLabel}: ${format(safeEntitled)}. ${over > 0 ? `${format(over)} ${overLabel}` : `${format(available)} ${availableLabel}`}.`;
25
+ return /* @__PURE__ */ jsxs("div", { className: cx("uix-license-position", over > 0 && "uix-license-position--over", className), ...props, children: [
26
+ /* @__PURE__ */ jsxs("div", { className: "uix-license-position__header", children: [
27
+ /* @__PURE__ */ jsxs("span", { className: "uix-license-position__value", children: [
28
+ format(safeConsumed),
29
+ " / ",
30
+ format(safeEntitled)
31
+ ] }),
32
+ /* @__PURE__ */ jsx(StatusPill, { tone: over > 0 ? "danger" : percent >= 80 ? "warning" : "success", children: over > 0 ? `${format(over)} ${overLabel}` : `${format(available)} ${availableLabel}` })
33
+ ] }),
34
+ /* @__PURE__ */ jsxs("div", { className: "uix-license-position__track", children: [
35
+ /* @__PURE__ */ jsx(Meter, { value: Math.min(100, percent), tone: over > 0 ? "danger" : percent >= 80 ? "warning" : "success", "aria-label": summary, "aria-valuetext": summary }),
36
+ over > 0 && /* @__PURE__ */ jsx(
37
+ "span",
38
+ {
39
+ className: "uix-license-position__overflow",
40
+ style: { width: `${Math.min(100, safeEntitled === 0 ? 100 : over / safeEntitled * 100)}%` },
41
+ "aria-hidden": "true"
42
+ }
43
+ )
44
+ ] }),
45
+ /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: summary })
46
+ ] });
47
+ }
48
+ export {
49
+ LicensePositionBar
50
+ };
@@ -0,0 +1,39 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { JsonValue } from '../json-value.js';
4
+
5
+ interface MatchReviewField {
6
+ id: string;
7
+ label: string;
8
+ render?: (value: JsonValue | undefined, record: Record<string, JsonValue>) => ReactNode;
9
+ }
10
+ interface MatchReviewCandidate {
11
+ id: string;
12
+ record: Record<string, JsonValue>;
13
+ confidence?: number;
14
+ status?: 'pending' | 'resolved';
15
+ decision?: 'accepted' | 'dismissed' | 'split' | 'other';
16
+ }
17
+ type MatchReviewAction = 'accept' | 'dismiss' | 'split' | 'pick-other';
18
+ interface MatchBulkResult {
19
+ succeeded: number;
20
+ failed?: number;
21
+ }
22
+ interface MatchReviewProps {
23
+ incoming: Record<string, JsonValue>;
24
+ incomingLabel?: string;
25
+ fields: MatchReviewField[];
26
+ candidates: MatchReviewCandidate[];
27
+ selectedIds?: Set<string>;
28
+ onSelectionChange?: (ids: Set<string>) => void;
29
+ onDecision: (candidateId: string, action: MatchReviewAction) => void;
30
+ onBulkDecision?: (candidateIds: string[], action: Extract<MatchReviewAction, 'accept' | 'dismiss'>) => void | MatchBulkResult | Promise<void | MatchBulkResult>;
31
+ loading?: boolean;
32
+ error?: string;
33
+ onRetry?: () => void;
34
+ className?: string;
35
+ }
36
+ /** Generic descriptor-driven incoming-record to candidate-match decision surface. */
37
+ declare function MatchReview({ incoming, incomingLabel, fields, candidates, selectedIds, onSelectionChange, onDecision, onBulkDecision, loading, error, onRetry, className, }: MatchReviewProps): react.JSX.Element;
38
+
39
+ export { type MatchBulkResult, MatchReview, type MatchReviewAction, type MatchReviewCandidate, type MatchReviewField, type MatchReviewProps };
@@ -0,0 +1,120 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from "react";
4
+ import { cx } from "../cx.js";
5
+ import { StatusPill } from "./StatusPill.js";
6
+ const valueText = (value) => value == null ? "\u2014" : typeof value === "string" ? value : JSON.stringify(value);
7
+ function MatchReview({
8
+ incoming,
9
+ incomingLabel = "Incoming record",
10
+ fields,
11
+ candidates,
12
+ selectedIds,
13
+ onSelectionChange,
14
+ onDecision,
15
+ onBulkDecision,
16
+ loading,
17
+ error,
18
+ onRetry,
19
+ className
20
+ }) {
21
+ const [internalSelection, setInternalSelection] = useState(/* @__PURE__ */ new Set());
22
+ const [announcement, setAnnouncement] = useState("");
23
+ const [bulkPending, setBulkPending] = useState(false);
24
+ const selection = selectedIds ?? internalSelection;
25
+ const pending = useMemo(() => candidates.filter((candidate) => candidate.status !== "resolved"), [candidates]);
26
+ const setSelection = (next) => {
27
+ if (selectedIds === void 0) setInternalSelection(next);
28
+ onSelectionChange?.(next);
29
+ };
30
+ const toggle = (id) => {
31
+ const next = new Set(selection);
32
+ if (next.has(id)) next.delete(id);
33
+ else next.add(id);
34
+ setSelection(next);
35
+ };
36
+ const runBulk = async (action) => {
37
+ if (!onBulkDecision || selection.size === 0 || bulkPending) return;
38
+ const ids = [...selection];
39
+ setBulkPending(true);
40
+ setAnnouncement(`${action === "accept" ? "Accepting" : "Dismissing"} ${ids.length} candidate${ids.length === 1 ? "" : "s"}\u2026`);
41
+ try {
42
+ const result = await onBulkDecision(ids, action);
43
+ const succeeded = result?.succeeded ?? ids.length;
44
+ const failed = result?.failed ?? 0;
45
+ setAnnouncement(`${succeeded} candidate${succeeded === 1 ? "" : "s"} ${action === "accept" ? "accepted" : "dismissed"}${failed ? `; ${failed} failed` : ""}.`);
46
+ if (!failed) setSelection(/* @__PURE__ */ new Set());
47
+ } catch {
48
+ setAnnouncement(`Bulk ${action} failed. Try again.`);
49
+ } finally {
50
+ setBulkPending(false);
51
+ }
52
+ };
53
+ if (loading) return /* @__PURE__ */ jsx("div", { className: cx("uix-match-review uix-match-review--state", className), role: "status", children: "Loading match candidates\u2026" });
54
+ if (error) return /* @__PURE__ */ jsxs("div", { className: cx("uix-match-review uix-match-review--state", className), role: "alert", children: [
55
+ /* @__PURE__ */ jsx("p", { children: error }),
56
+ onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: "Try again" })
57
+ ] });
58
+ if (candidates.length === 0) return /* @__PURE__ */ jsxs("div", { className: cx("uix-match-review uix-match-review--state", className), children: [
59
+ /* @__PURE__ */ jsx("p", { children: "No candidate matches found." }),
60
+ /* @__PURE__ */ jsx("p", { children: "Choose another record source or continue without a match." })
61
+ ] });
62
+ return /* @__PURE__ */ jsxs("section", { className: cx("uix-match-review", className), "aria-label": "Match review", children: [
63
+ /* @__PURE__ */ jsxs("div", { className: "uix-match-review__summary", children: [
64
+ /* @__PURE__ */ jsxs("div", { children: [
65
+ /* @__PURE__ */ jsx("strong", { children: incomingLabel }),
66
+ /* @__PURE__ */ jsxs("span", { children: [
67
+ pending.length,
68
+ " pending of ",
69
+ candidates.length
70
+ ] })
71
+ ] }),
72
+ /* @__PURE__ */ jsx("div", { className: "uix-match-review__incoming", children: fields.map((field) => /* @__PURE__ */ jsxs("span", { children: [
73
+ /* @__PURE__ */ jsx("b", { children: field.label }),
74
+ field.render?.(incoming[field.id], incoming) ?? valueText(incoming[field.id])
75
+ ] }, field.id)) })
76
+ ] }),
77
+ onBulkDecision && /* @__PURE__ */ jsxs("div", { className: "uix-match-review__bulk", "aria-label": "Bulk candidate actions", children: [
78
+ /* @__PURE__ */ jsxs("span", { children: [
79
+ selection.size,
80
+ " selected"
81
+ ] }),
82
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary uix-btn--sm", disabled: selection.size === 0 || bulkPending, onClick: () => void runBulk("accept"), children: bulkPending ? "Applying\u2026" : "Accept selected" }),
83
+ /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", disabled: selection.size === 0 || bulkPending, onClick: () => void runBulk("dismiss"), children: bulkPending ? "Applying\u2026" : "Dismiss selected" })
84
+ ] }),
85
+ /* @__PURE__ */ jsx("div", { className: "uix-match-review__table-wrap", children: /* @__PURE__ */ jsxs("table", { className: "uix-table uix-match-review__table", children: [
86
+ /* @__PURE__ */ jsx("caption", { className: "uix-visually-hidden", children: "Candidate matches and available decisions" }),
87
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
88
+ /* @__PURE__ */ jsx("th", { scope: "col", children: /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: "Select" }) }),
89
+ /* @__PURE__ */ jsx("th", { scope: "col", children: "Confidence" }),
90
+ fields.map((field) => /* @__PURE__ */ jsx("th", { scope: "col", children: field.label }, field.id)),
91
+ /* @__PURE__ */ jsx("th", { scope: "col", children: "Status" }),
92
+ /* @__PURE__ */ jsx("th", { scope: "col", children: "Actions" })
93
+ ] }) }),
94
+ /* @__PURE__ */ jsx("tbody", { children: candidates.map((candidate) => {
95
+ const resolved = candidate.status === "resolved";
96
+ return /* @__PURE__ */ jsxs("tr", { "data-status": candidate.status ?? "pending", children: [
97
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("input", { type: "checkbox", checked: selection.has(candidate.id), onChange: () => toggle(candidate.id), "aria-label": `Select candidate ${candidate.id}` }) }),
98
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("span", { className: "uix-match-review__confidence", children: candidate.confidence == null ? "Not scored" : `${Math.round(candidate.confidence * 100)}%` }) }),
99
+ fields.map((field) => /* @__PURE__ */ jsx("td", { children: field.render?.(candidate.record[field.id], candidate.record) ?? valueText(candidate.record[field.id]) }, field.id)),
100
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(StatusPill, { tone: resolved ? "success" : "warning", children: resolved ? candidate.decision ?? "Resolved" : "Pending" }) }),
101
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("div", { className: "uix-match-review__actions", children: ["accept", "dismiss", "split", "pick-other"].map((action) => /* @__PURE__ */ jsx(
102
+ "button",
103
+ {
104
+ type: "button",
105
+ className: "uix-btn uix-btn--ghost uix-btn--sm",
106
+ disabled: resolved,
107
+ onClick: () => onDecision(candidate.id, action),
108
+ children: action === "pick-other" ? "Pick other" : `${action[0].toUpperCase()}${action.slice(1)}`
109
+ },
110
+ action
111
+ )) }) })
112
+ ] }, candidate.id);
113
+ }) })
114
+ ] }) }),
115
+ /* @__PURE__ */ jsx("p", { className: "uix-visually-hidden", "aria-live": "polite", children: announcement })
116
+ ] });
117
+ }
118
+ export {
119
+ MatchReview
120
+ };