@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.
- package/README.md +16 -0
- package/dist/calendar-model.d.ts +31 -0
- package/dist/calendar-model.js +89 -0
- package/dist/color-model.d.ts +20 -0
- package/dist/color-model.js +74 -0
- package/dist/components/BrandProfiles.d.ts +45 -0
- package/dist/components/BrandProfiles.js +150 -0
- package/dist/components/BuilderCanvas.d.ts +34 -0
- package/dist/components/BuilderCanvas.js +117 -0
- package/dist/components/ColorPicker.d.ts +17 -0
- package/dist/components/ColorPicker.js +158 -0
- package/dist/components/DateRangePicker.d.ts +23 -0
- package/dist/components/DateRangePicker.js +123 -0
- package/dist/components/DiffViewer.d.ts +18 -0
- package/dist/components/DiffViewer.js +93 -0
- package/dist/components/LicensePositionBar.d.ts +17 -0
- package/dist/components/LicensePositionBar.js +50 -0
- package/dist/components/MatchReview.d.ts +39 -0
- package/dist/components/MatchReview.js +120 -0
- package/dist/components/MetricInput.d.ts +23 -0
- package/dist/components/MetricInput.js +92 -0
- package/dist/components/RelationshipGraph.d.ts +30 -0
- package/dist/components/RelationshipGraph.js +138 -0
- package/dist/components/RuleBuilder.d.ts +45 -0
- package/dist/components/RuleBuilder.js +163 -0
- package/dist/components/SchedulingCalendar.d.ts +42 -0
- package/dist/components/SchedulingCalendar.js +124 -0
- package/dist/diff-model.d.ts +23 -0
- package/dist/diff-model.js +50 -0
- package/dist/index.cjs +1753 -2
- package/dist/index.d.cts +439 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.js +91 -1
- package/dist/json-value.d.ts +10 -0
- package/dist/json-value.js +19 -0
- package/dist/metric-model.d.ts +9 -0
- package/dist/metric-model.js +25 -0
- package/dist/relationship-graph-model.d.ts +31 -0
- package/dist/relationship-graph-model.js +68 -0
- package/dist/rule-builder-model.d.ts +41 -0
- package/dist/rule-builder-model.js +101 -0
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
interface MetricInputProps {
|
|
4
|
+
value: number | null;
|
|
5
|
+
onValueChange: (value: number | null) => void;
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
step?: number;
|
|
9
|
+
unit?: string;
|
|
10
|
+
label: string;
|
|
11
|
+
invalid?: boolean;
|
|
12
|
+
errorMessage?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
readOnly?: boolean;
|
|
15
|
+
parse?: (input: string) => number | null;
|
|
16
|
+
format?: (value: number | null) => string;
|
|
17
|
+
className?: string;
|
|
18
|
+
id?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Accessible numeric recipe composed from Input and the UIx stepper controls. */
|
|
21
|
+
declare function MetricInput({ value, onValueChange, min, max, step, unit, label, invalid, errorMessage, disabled, readOnly, parse, format, className, id: providedId, }: MetricInputProps): react.JSX.Element;
|
|
22
|
+
|
|
23
|
+
export { MetricInput, type MetricInputProps };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useState } from "react";
|
|
4
|
+
import { cx } from "../cx.js";
|
|
5
|
+
import { clampMetricValue, parseMetricValue, stepMetricValue } from "../metric-model.js";
|
|
6
|
+
import { Input } from "./Input.js";
|
|
7
|
+
const defaultMetricFormat = (input) => input == null ? "" : String(input);
|
|
8
|
+
function MetricInput({
|
|
9
|
+
value,
|
|
10
|
+
onValueChange,
|
|
11
|
+
min,
|
|
12
|
+
max,
|
|
13
|
+
step = 1,
|
|
14
|
+
unit,
|
|
15
|
+
label,
|
|
16
|
+
invalid,
|
|
17
|
+
errorMessage,
|
|
18
|
+
disabled,
|
|
19
|
+
readOnly,
|
|
20
|
+
parse = parseMetricValue,
|
|
21
|
+
format = defaultMetricFormat,
|
|
22
|
+
className,
|
|
23
|
+
id: providedId
|
|
24
|
+
}) {
|
|
25
|
+
const generatedId = useId();
|
|
26
|
+
const id = providedId ?? generatedId;
|
|
27
|
+
const errorId = `${id}-error`;
|
|
28
|
+
const [text, setText] = useState(() => format(value));
|
|
29
|
+
useEffect(() => setText(format(value)), [value, format]);
|
|
30
|
+
const commit = (input) => {
|
|
31
|
+
const parsed = parse(input);
|
|
32
|
+
if (input.trim() === "" || parsed !== null) {
|
|
33
|
+
onValueChange(parsed === null ? null : clampMetricValue(parsed, min, max));
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const stepBy = (direction) => {
|
|
37
|
+
const next = stepMetricValue(value, direction, { min, max, step });
|
|
38
|
+
onValueChange(next);
|
|
39
|
+
setText(format(next));
|
|
40
|
+
};
|
|
41
|
+
const onKeyDown = (event) => {
|
|
42
|
+
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
stepBy(event.key === "ArrowUp" ? 1 : -1);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const onChange = (event) => {
|
|
48
|
+
setText(event.currentTarget.value);
|
|
49
|
+
commit(event.currentTarget.value);
|
|
50
|
+
};
|
|
51
|
+
const onBlur = (event) => {
|
|
52
|
+
const parsed = parse(event.currentTarget.value);
|
|
53
|
+
setText(parsed === null && event.currentTarget.value.trim() !== "" ? format(value) : format(parsed));
|
|
54
|
+
};
|
|
55
|
+
return /* @__PURE__ */ jsxs("div", { className: cx("uix-metric-input", invalid && "uix-metric-input--invalid", className), children: [
|
|
56
|
+
/* @__PURE__ */ jsx("label", { className: "uix-field__label", htmlFor: id, children: label }),
|
|
57
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-metric-input__control", children: [
|
|
58
|
+
/* @__PURE__ */ jsx(
|
|
59
|
+
Input,
|
|
60
|
+
{
|
|
61
|
+
id,
|
|
62
|
+
value: text,
|
|
63
|
+
onChange,
|
|
64
|
+
onBlur,
|
|
65
|
+
onKeyDown,
|
|
66
|
+
inputMode: "decimal",
|
|
67
|
+
role: "spinbutton",
|
|
68
|
+
"aria-valuenow": value ?? void 0,
|
|
69
|
+
"aria-valuemin": min,
|
|
70
|
+
"aria-valuemax": max,
|
|
71
|
+
"aria-describedby": invalid && errorMessage ? errorId : void 0,
|
|
72
|
+
invalid,
|
|
73
|
+
disabled,
|
|
74
|
+
readOnly
|
|
75
|
+
}
|
|
76
|
+
),
|
|
77
|
+
unit && /* @__PURE__ */ jsx("span", { className: "uix-metric-input__unit", "aria-hidden": "true", children: unit }),
|
|
78
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-metric-input__steps", children: [
|
|
79
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-metric-input__step", onClick: () => stepBy(1), disabled: disabled || readOnly, "aria-label": `Increase ${label}`, children: "+" }),
|
|
80
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-metric-input__step", onClick: () => stepBy(-1), disabled: disabled || readOnly, "aria-label": `Decrease ${label}`, children: "\u2212" })
|
|
81
|
+
] })
|
|
82
|
+
] }),
|
|
83
|
+
unit && /* @__PURE__ */ jsxs("span", { className: "uix-visually-hidden", children: [
|
|
84
|
+
"Unit: ",
|
|
85
|
+
unit
|
|
86
|
+
] }),
|
|
87
|
+
invalid && errorMessage && /* @__PURE__ */ jsx("span", { id: errorId, className: "uix-field__error", children: errorMessage })
|
|
88
|
+
] });
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
MetricInput
|
|
92
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { RelationshipGraphNode, RelationshipGraphEdge } from '../relationship-graph-model.js';
|
|
3
|
+
|
|
4
|
+
interface RelationshipGraphLegendItem {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
interface RelationshipGraphProps {
|
|
9
|
+
nodes: RelationshipGraphNode[];
|
|
10
|
+
edges: RelationshipGraphEdge[];
|
|
11
|
+
selectedId?: string;
|
|
12
|
+
onSelect?: (node: RelationshipGraphNode) => void;
|
|
13
|
+
onExpandNeighbors?: (node: RelationshipGraphNode) => void;
|
|
14
|
+
onOpenDetails?: (node: RelationshipGraphNode) => void;
|
|
15
|
+
maxNodes?: number;
|
|
16
|
+
density?: 'compact' | 'standard';
|
|
17
|
+
highlightedNodeIds?: ReadonlySet<string>;
|
|
18
|
+
conflictedNodeIds?: ReadonlySet<string>;
|
|
19
|
+
highlightedEdgeIds?: ReadonlySet<string>;
|
|
20
|
+
conflictedEdgeIds?: ReadonlySet<string>;
|
|
21
|
+
legend?: RelationshipGraphLegendItem[];
|
|
22
|
+
loading?: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
onRetry?: () => void;
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Bounded, deterministic SVG relationship view with an equivalent accessible list. */
|
|
28
|
+
declare function RelationshipGraph({ nodes, edges, selectedId, onSelect, onExpandNeighbors, onOpenDetails, maxNodes, density, highlightedNodeIds, conflictedNodeIds, highlightedEdgeIds, conflictedEdgeIds, legend, loading, error, onRetry, className, }: RelationshipGraphProps): react.JSX.Element;
|
|
29
|
+
|
|
30
|
+
export { RelationshipGraph, type RelationshipGraphLegendItem, type RelationshipGraphProps };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo, useRef, useState } from "react";
|
|
4
|
+
import { boundRelationshipGraph, layoutRelationshipGraph, relationshipNeighbors, traverseRelationshipNode } from "../relationship-graph-model.js";
|
|
5
|
+
import { cx } from "../cx.js";
|
|
6
|
+
const EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
7
|
+
const EMPTY_LEGEND = [];
|
|
8
|
+
function RelationshipGraph({
|
|
9
|
+
nodes,
|
|
10
|
+
edges,
|
|
11
|
+
selectedId,
|
|
12
|
+
onSelect,
|
|
13
|
+
onExpandNeighbors,
|
|
14
|
+
onOpenDetails,
|
|
15
|
+
maxNodes = 40,
|
|
16
|
+
density = "standard",
|
|
17
|
+
highlightedNodeIds = EMPTY_IDS,
|
|
18
|
+
conflictedNodeIds = EMPTY_IDS,
|
|
19
|
+
highlightedEdgeIds = EMPTY_IDS,
|
|
20
|
+
conflictedEdgeIds = EMPTY_IDS,
|
|
21
|
+
legend = EMPTY_LEGEND,
|
|
22
|
+
loading,
|
|
23
|
+
error,
|
|
24
|
+
onRetry,
|
|
25
|
+
className
|
|
26
|
+
}) {
|
|
27
|
+
const bounded = useMemo(() => boundRelationshipGraph(nodes, edges, maxNodes), [nodes, edges, maxNodes]);
|
|
28
|
+
const positioned = useMemo(() => layoutRelationshipGraph(bounded.nodes, bounded.edges, selectedId), [bounded, selectedId]);
|
|
29
|
+
const positionedById = useMemo(() => new Map(positioned.map((node) => [node.id, node])), [positioned]);
|
|
30
|
+
const [zoom, setZoom] = useState(1);
|
|
31
|
+
const [pan, setPan] = useState({ x: 0, y: 0 });
|
|
32
|
+
const [focusedId, setFocusedId] = useState(selectedId ?? positioned[0]?.id);
|
|
33
|
+
const svgRef = useRef(null);
|
|
34
|
+
const select = (id) => {
|
|
35
|
+
const node = bounded.nodes.find((item) => item.id === id);
|
|
36
|
+
if (node) onSelect?.(node);
|
|
37
|
+
};
|
|
38
|
+
const focusNode = (id) => {
|
|
39
|
+
if (!id) return;
|
|
40
|
+
setFocusedId(id);
|
|
41
|
+
requestAnimationFrame(() => svgRef.current?.querySelector(`[data-node-id="${id}"]`)?.focus());
|
|
42
|
+
};
|
|
43
|
+
const onNodeKeyDown = (event, id) => {
|
|
44
|
+
if (["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(event.key)) {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
focusNode(traverseRelationshipNode(positioned, id, event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : -1));
|
|
47
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
select(id);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const reset = () => {
|
|
53
|
+
setZoom(1);
|
|
54
|
+
setPan({ x: 0, y: 0 });
|
|
55
|
+
};
|
|
56
|
+
if (loading) return /* @__PURE__ */ jsx("div", { className: cx("uix-relationship-graph uix-relationship-graph--state", className), role: "status", children: "Loading relationships\u2026" });
|
|
57
|
+
if (error) return /* @__PURE__ */ jsxs("div", { className: cx("uix-relationship-graph uix-relationship-graph--state", className), role: "alert", children: [
|
|
58
|
+
/* @__PURE__ */ jsx("p", { children: error }),
|
|
59
|
+
onRetry && /* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: "Try again" })
|
|
60
|
+
] });
|
|
61
|
+
if (bounded.nodes.length === 0) return /* @__PURE__ */ jsxs("div", { className: cx("uix-relationship-graph uix-relationship-graph--state", className), children: [
|
|
62
|
+
/* @__PURE__ */ jsx("p", { children: "No relationships to display." }),
|
|
63
|
+
/* @__PURE__ */ jsx("p", { children: "Adjust the neighborhood or filters to include a node." })
|
|
64
|
+
] });
|
|
65
|
+
return /* @__PURE__ */ jsxs("section", { className: cx("uix-relationship-graph", `uix-relationship-graph--${density}`, className), "aria-label": "Relationship graph", children: [
|
|
66
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-relationship-graph__toolbar", "aria-label": "Graph view controls", children: [
|
|
67
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, x: value.x - 24 })), "aria-label": "Pan left", children: "Left" }),
|
|
68
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, y: value.y - 24 })), "aria-label": "Pan up", children: "Up" }),
|
|
69
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, y: value.y + 24 })), "aria-label": "Pan down", children: "Down" }),
|
|
70
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPan((value) => ({ ...value, x: value.x + 24 })), "aria-label": "Pan right", children: "Right" }),
|
|
71
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setZoom((value) => Math.min(2, Number((value + 0.2).toFixed(1)))), "aria-label": "Zoom in", children: "Zoom in" }),
|
|
72
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setZoom((value) => Math.max(0.5, Number((value - 0.2).toFixed(1)))), "aria-label": "Zoom out", children: "Zoom out" }),
|
|
73
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: reset, children: "Reset view" }),
|
|
74
|
+
/* @__PURE__ */ jsxs("span", { "aria-live": "polite", children: [
|
|
75
|
+
"Zoom ",
|
|
76
|
+
Math.round(zoom * 100),
|
|
77
|
+
"%"
|
|
78
|
+
] })
|
|
79
|
+
] }),
|
|
80
|
+
bounded.omittedNodeCount > 0 && /* @__PURE__ */ jsxs("p", { className: "uix-relationship-graph__bounded", role: "status", children: [
|
|
81
|
+
"Showing ",
|
|
82
|
+
bounded.nodes.length,
|
|
83
|
+
" nodes; ",
|
|
84
|
+
bounded.omittedNodeCount,
|
|
85
|
+
" outside this bounded neighborhood."
|
|
86
|
+
] }),
|
|
87
|
+
/* @__PURE__ */ jsx("div", { className: "uix-relationship-graph__visual", children: /* @__PURE__ */ jsx("svg", { ref: svgRef, viewBox: "0 0 600 400", role: "group", "aria-label": `Graph with ${bounded.nodes.length} nodes and ${bounded.edges.length} relationships`, children: /* @__PURE__ */ jsxs("g", { transform: `translate(${pan.x} ${pan.y}) scale(${zoom})`, children: [
|
|
88
|
+
bounded.edges.map((edge) => {
|
|
89
|
+
const source = positionedById.get(edge.source);
|
|
90
|
+
const target = positionedById.get(edge.target);
|
|
91
|
+
return /* @__PURE__ */ jsx("line", { x1: source.x * 6, y1: source.y * 4, x2: target.x * 6, y2: target.y * 4, className: "uix-relationship-graph__edge", "data-highlighted": highlightedEdgeIds.has(edge.id) || void 0, "data-conflicted": conflictedEdgeIds.has(edge.id) || void 0, children: /* @__PURE__ */ jsx("title", { children: edge.label ?? edge.type ?? "Relationship" }) }, edge.id);
|
|
92
|
+
}),
|
|
93
|
+
positioned.map((node) => /* @__PURE__ */ jsxs(
|
|
94
|
+
"g",
|
|
95
|
+
{
|
|
96
|
+
transform: `translate(${node.x * 6} ${node.y * 4})`,
|
|
97
|
+
role: "button",
|
|
98
|
+
"aria-label": `${node.label}${node.type ? `, ${node.type}` : ""}${conflictedNodeIds.has(node.id) ? ", conflict" : ""}`,
|
|
99
|
+
tabIndex: focusedId === node.id ? 0 : -1,
|
|
100
|
+
"data-node-id": node.id,
|
|
101
|
+
className: "uix-relationship-graph__node",
|
|
102
|
+
"data-selected": selectedId === node.id || void 0,
|
|
103
|
+
"data-highlighted": highlightedNodeIds.has(node.id) || void 0,
|
|
104
|
+
"data-conflicted": conflictedNodeIds.has(node.id) || void 0,
|
|
105
|
+
onFocus: () => setFocusedId(node.id),
|
|
106
|
+
onKeyDown: (event) => onNodeKeyDown(event, node.id),
|
|
107
|
+
onClick: () => select(node.id),
|
|
108
|
+
children: [
|
|
109
|
+
/* @__PURE__ */ jsx("rect", { x: "-55", y: "-18", width: "110", height: "36", rx: "8" }),
|
|
110
|
+
/* @__PURE__ */ jsx("text", { textAnchor: "middle", dominantBaseline: "middle", children: node.label.length > 16 ? `${node.label.slice(0, 15)}\u2026` : node.label })
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
node.id
|
|
114
|
+
))
|
|
115
|
+
] }) }) }),
|
|
116
|
+
legend.length > 0 && /* @__PURE__ */ jsx("ul", { className: "uix-relationship-graph__legend", "aria-label": "Relationship types", children: legend.map((item) => /* @__PURE__ */ jsx("li", { "data-type": item.id, children: item.label }, item.id)) }),
|
|
117
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-relationship-graph__list", role: "region", "aria-label": "Accessible relationship list", children: [
|
|
118
|
+
/* @__PURE__ */ jsx("h3", { children: "Relationship list" }),
|
|
119
|
+
/* @__PURE__ */ jsx("ul", { children: bounded.nodes.map((node) => {
|
|
120
|
+
const neighbors = relationshipNeighbors(node.id, bounded.edges).map((id) => bounded.nodes.find((item) => item.id === id)?.label ?? id);
|
|
121
|
+
return /* @__PURE__ */ jsxs("li", { "data-selected": selectedId === node.id || void 0, children: [
|
|
122
|
+
/* @__PURE__ */ jsxs("button", { type: "button", onClick: () => select(node.id), "aria-pressed": selectedId === node.id, children: [
|
|
123
|
+
/* @__PURE__ */ jsx("strong", { children: node.label }),
|
|
124
|
+
node.type && /* @__PURE__ */ jsx("span", { children: node.type }),
|
|
125
|
+
/* @__PURE__ */ jsx("span", { children: neighbors.length ? `Connected to ${neighbors.join(", ")}` : "No visible connections" })
|
|
126
|
+
] }),
|
|
127
|
+
/* @__PURE__ */ jsxs("span", { className: "uix-relationship-graph__list-actions", children: [
|
|
128
|
+
onExpandNeighbors && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onExpandNeighbors(node), children: "Expand neighbors" }),
|
|
129
|
+
onOpenDetails && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => onOpenDetails(node), children: "Open details" })
|
|
130
|
+
] })
|
|
131
|
+
] }, node.id);
|
|
132
|
+
}) })
|
|
133
|
+
] })
|
|
134
|
+
] });
|
|
135
|
+
}
|
|
136
|
+
export {
|
|
137
|
+
RelationshipGraph
|
|
138
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { JsonValue } from '../json-value.js';
|
|
4
|
+
import { RuleAction, RuleDefinition, RuleCondition, RuleValidationIssue } from '../rule-builder-model.js';
|
|
5
|
+
|
|
6
|
+
interface RuleValueEditorProps {
|
|
7
|
+
condition: RuleCondition;
|
|
8
|
+
onChange: (value: JsonValue | undefined) => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface RuleFieldDefinition {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
operators?: string[];
|
|
16
|
+
renderValueEditor?: (props: RuleValueEditorProps) => ReactNode;
|
|
17
|
+
}
|
|
18
|
+
interface RuleOperatorDefinition {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
requiresValue?: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface RuleActionDefinition {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
renderParameters?: (action: RuleAction, onChange: (parameters: Record<string, JsonValue>) => void, disabled: boolean) => ReactNode;
|
|
27
|
+
}
|
|
28
|
+
interface RuleBuilderProps {
|
|
29
|
+
value: RuleDefinition;
|
|
30
|
+
onChange: (value: RuleDefinition) => void;
|
|
31
|
+
fields: RuleFieldDefinition[];
|
|
32
|
+
operators: RuleOperatorDefinition[];
|
|
33
|
+
actions: RuleActionDefinition[];
|
|
34
|
+
maxDepth?: number;
|
|
35
|
+
readOnly?: boolean;
|
|
36
|
+
validate?: (value: RuleDefinition) => RuleValidationIssue[];
|
|
37
|
+
className?: string;
|
|
38
|
+
addConditionLabel?: string;
|
|
39
|
+
addGroupLabel?: string;
|
|
40
|
+
addActionLabel?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Declarative controlled when-conditions-to-actions editor with bounded nesting. */
|
|
43
|
+
declare function RuleBuilder({ value, onChange, fields, operators, actions, maxDepth, readOnly, validate, className, addConditionLabel, addGroupLabel, addActionLabel, }: RuleBuilderProps): react.JSX.Element;
|
|
44
|
+
|
|
45
|
+
export { type RuleActionDefinition, RuleBuilder, type RuleBuilderProps, type RuleFieldDefinition, type RuleOperatorDefinition, type RuleValueEditorProps };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cx } from "../cx.js";
|
|
4
|
+
import {
|
|
5
|
+
appendRuleNode,
|
|
6
|
+
isRuleGroup,
|
|
7
|
+
mapRuleGroup,
|
|
8
|
+
moveRuleNode,
|
|
9
|
+
removeRuleNode,
|
|
10
|
+
summarizeRule,
|
|
11
|
+
validateRuleDefinition
|
|
12
|
+
} from "../rule-builder-model.js";
|
|
13
|
+
let ruleId = 0;
|
|
14
|
+
const nextId = (prefix) => `${prefix}-${Date.now().toString(36)}-${(++ruleId).toString(36)}`;
|
|
15
|
+
function defaultCondition(fields, operators) {
|
|
16
|
+
const field = fields[0];
|
|
17
|
+
const operator = operators.find((item) => field?.operators?.includes(item.id)) ?? operators[0];
|
|
18
|
+
return { id: nextId("condition"), field: field?.id ?? "", operator: operator?.id ?? "", value: "" };
|
|
19
|
+
}
|
|
20
|
+
function RuleGroupEditor({ group, depth, root, fields, operators, maxDepth, disabled, onRootChange, labels }) {
|
|
21
|
+
const updateNode = (id, update) => onRootChange(mapRuleGroup(root, id, update));
|
|
22
|
+
const addCondition = () => onRootChange(appendRuleNode(root, group.id, defaultCondition(fields, operators), maxDepth));
|
|
23
|
+
const addGroup = () => onRootChange(appendRuleNode(root, group.id, {
|
|
24
|
+
id: nextId("group"),
|
|
25
|
+
combinator: "and",
|
|
26
|
+
conditions: [defaultCondition(fields, operators)]
|
|
27
|
+
}, maxDepth));
|
|
28
|
+
return /* @__PURE__ */ jsxs("fieldset", { className: "uix-rule-builder__group", "data-depth": depth, children: [
|
|
29
|
+
/* @__PURE__ */ jsxs("legend", { className: "uix-rule-builder__group-heading", children: [
|
|
30
|
+
/* @__PURE__ */ jsx("span", { children: depth === 1 ? "When" : "Group" }),
|
|
31
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
32
|
+
"Match",
|
|
33
|
+
/* @__PURE__ */ jsxs("select", { className: "uix-select uix-select--sm", value: group.combinator, disabled, onChange: (event) => updateNode(group.id, (node) => ({ ...node, combinator: event.currentTarget.value })), children: [
|
|
34
|
+
/* @__PURE__ */ jsx("option", { value: "and", children: "all (AND)" }),
|
|
35
|
+
/* @__PURE__ */ jsx("option", { value: "or", children: "any (OR)" })
|
|
36
|
+
] })
|
|
37
|
+
] })
|
|
38
|
+
] }),
|
|
39
|
+
/* @__PURE__ */ jsx("div", { className: "uix-rule-builder__rows", children: group.conditions.map((node, index) => isRuleGroup(node) ? /* @__PURE__ */ jsxs("div", { className: "uix-rule-builder__nested", children: [
|
|
40
|
+
/* @__PURE__ */ jsx(RuleGroupEditor, { ...{ group: node, depth: depth + 1, root, fields, operators, maxDepth, disabled, onRootChange, labels } }),
|
|
41
|
+
/* @__PURE__ */ jsx(RowControls, { label: "group", first: index === 0, last: index === group.conditions.length - 1, disabled, onUp: () => onRootChange(moveRuleNode(root, node.id, -1)), onDown: () => onRootChange(moveRuleNode(root, node.id, 1)), onRemove: () => onRootChange(removeRuleNode(root, node.id)) })
|
|
42
|
+
] }, node.id) : /* @__PURE__ */ jsx(
|
|
43
|
+
ConditionRow,
|
|
44
|
+
{
|
|
45
|
+
condition: node,
|
|
46
|
+
fields,
|
|
47
|
+
operators,
|
|
48
|
+
disabled,
|
|
49
|
+
onChange: (condition) => updateNode(node.id, () => condition),
|
|
50
|
+
controls: /* @__PURE__ */ jsx(RowControls, { label: "condition", first: index === 0, last: index === group.conditions.length - 1, disabled, onUp: () => onRootChange(moveRuleNode(root, node.id, -1)), onDown: () => onRootChange(moveRuleNode(root, node.id, 1)), onRemove: () => onRootChange(removeRuleNode(root, node.id)) })
|
|
51
|
+
},
|
|
52
|
+
node.id
|
|
53
|
+
)) }),
|
|
54
|
+
/* @__PURE__ */ jsxs("div", { className: "uix-rule-builder__add", children: [
|
|
55
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary uix-btn--sm", onClick: addCondition, disabled: disabled || fields.length === 0 || operators.length === 0, children: labels.addCondition }),
|
|
56
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: addGroup, disabled: disabled || depth >= maxDepth, children: labels.addGroup })
|
|
57
|
+
] })
|
|
58
|
+
] });
|
|
59
|
+
}
|
|
60
|
+
function ConditionRow({ condition, fields, operators, disabled, onChange, controls }) {
|
|
61
|
+
const field = fields.find((item) => item.id === condition.field);
|
|
62
|
+
const availableOperators = field?.operators?.length ? operators.filter((item) => field.operators.includes(item.id)) : operators;
|
|
63
|
+
const operator = operators.find((item) => item.id === condition.operator);
|
|
64
|
+
const setField = (id) => {
|
|
65
|
+
const nextField = fields.find((item) => item.id === id);
|
|
66
|
+
const nextOperator = operators.find((item) => nextField?.operators?.includes(item.id)) ?? availableOperators[0] ?? operators[0];
|
|
67
|
+
onChange({ ...condition, field: id, operator: nextOperator?.id ?? "" });
|
|
68
|
+
};
|
|
69
|
+
return /* @__PURE__ */ jsxs("div", { className: "uix-rule-builder__condition", children: [
|
|
70
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
71
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: "Field" }),
|
|
72
|
+
/* @__PURE__ */ jsx("select", { className: "uix-select", value: condition.field, onChange: (event) => setField(event.currentTarget.value), disabled, children: fields.map((item) => /* @__PURE__ */ jsx("option", { value: item.id, children: item.label }, item.id)) })
|
|
73
|
+
] }),
|
|
74
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
75
|
+
/* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: "Operator" }),
|
|
76
|
+
/* @__PURE__ */ jsx("select", { className: "uix-select", value: condition.operator, onChange: (event) => onChange({ ...condition, operator: event.currentTarget.value }), disabled, children: availableOperators.map((item) => /* @__PURE__ */ jsx("option", { value: item.id, children: item.label }, item.id)) })
|
|
77
|
+
] }),
|
|
78
|
+
operator?.requiresValue !== false && /* @__PURE__ */ jsx("div", { className: "uix-rule-builder__value", children: field?.renderValueEditor?.({ condition, onChange: (value) => onChange({ ...condition, value }), disabled }) ?? /* @__PURE__ */ jsx("input", { className: "uix-input", value: typeof condition.value === "string" || typeof condition.value === "number" ? condition.value : condition.value == null ? "" : JSON.stringify(condition.value), onChange: (event) => onChange({ ...condition, value: event.currentTarget.value }), disabled, "aria-label": `Value for ${field?.label ?? "condition"}` }) }),
|
|
79
|
+
controls
|
|
80
|
+
] });
|
|
81
|
+
}
|
|
82
|
+
function RowControls({ label, first, last, disabled, onUp, onDown, onRemove }) {
|
|
83
|
+
return /* @__PURE__ */ jsxs("span", { className: "uix-rule-builder__row-actions", children: [
|
|
84
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: onUp, disabled: disabled || first, "aria-label": `Move ${label} up`, children: "Up" }),
|
|
85
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: onDown, disabled: disabled || last, "aria-label": `Move ${label} down`, children: "Down" }),
|
|
86
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: onRemove, disabled, "aria-label": `Remove ${label}`, children: "Remove" })
|
|
87
|
+
] });
|
|
88
|
+
}
|
|
89
|
+
function RuleBuilder({
|
|
90
|
+
value,
|
|
91
|
+
onChange,
|
|
92
|
+
fields,
|
|
93
|
+
operators,
|
|
94
|
+
actions,
|
|
95
|
+
maxDepth = 3,
|
|
96
|
+
readOnly = false,
|
|
97
|
+
validate,
|
|
98
|
+
className,
|
|
99
|
+
addConditionLabel = "Add condition",
|
|
100
|
+
addGroupLabel = "Add group",
|
|
101
|
+
addActionLabel = "Add action"
|
|
102
|
+
}) {
|
|
103
|
+
const issues = [...validateRuleDefinition(value, maxDepth), ...validate?.(value) ?? []];
|
|
104
|
+
const fieldLabels = Object.fromEntries(fields.map((field) => [field.id, field.label]));
|
|
105
|
+
const operatorLabels = Object.fromEntries(operators.map((operator) => [operator.id, operator.label]));
|
|
106
|
+
const actionLabels = Object.fromEntries(actions.map((action) => [action.id, action.label]));
|
|
107
|
+
if (readOnly) return /* @__PURE__ */ jsxs("section", { className: cx("uix-rule-builder uix-rule-builder--summary", className), "aria-label": "Rule summary", children: [
|
|
108
|
+
/* @__PURE__ */ jsx("p", { children: summarizeRule(value, { fields: fieldLabels, operators: operatorLabels, actions: actionLabels }) }),
|
|
109
|
+
issues.length > 0 && /* @__PURE__ */ jsxs("p", { className: "uix-rule-builder__invalid", children: [
|
|
110
|
+
"Rule has ",
|
|
111
|
+
issues.length,
|
|
112
|
+
" validation issue",
|
|
113
|
+
issues.length === 1 ? "" : "s",
|
|
114
|
+
"."
|
|
115
|
+
] })
|
|
116
|
+
] });
|
|
117
|
+
const updateActions = (next) => onChange({ ...value, then: next });
|
|
118
|
+
return /* @__PURE__ */ jsxs("section", { className: cx("uix-rule-builder", className), "aria-label": "Rule builder", children: [
|
|
119
|
+
/* @__PURE__ */ jsx(RuleGroupEditor, { group: value.when, depth: 1, root: value.when, fields, operators, maxDepth, disabled: readOnly, onRootChange: (when) => onChange({ ...value, when }), labels: { addCondition: addConditionLabel, addGroup: addGroupLabel } }),
|
|
120
|
+
/* @__PURE__ */ jsxs("fieldset", { className: "uix-rule-builder__actions", children: [
|
|
121
|
+
/* @__PURE__ */ jsx("legend", { children: "Then" }),
|
|
122
|
+
value.then.map((action, index) => {
|
|
123
|
+
const definition = actions.find((item) => item.id === action.type);
|
|
124
|
+
return /* @__PURE__ */ jsxs("div", { className: "uix-rule-builder__action", children: [
|
|
125
|
+
/* @__PURE__ */ jsx("select", { className: "uix-select", value: action.type, onChange: (event) => updateActions(value.then.map((item) => item.id === action.id ? { ...item, type: event.currentTarget.value } : item)), "aria-label": "Action type", children: actions.map((item) => /* @__PURE__ */ jsx("option", { value: item.id, children: item.label }, item.id)) }),
|
|
126
|
+
definition?.renderParameters?.(action, (parameters) => updateActions(value.then.map((item) => item.id === action.id ? { ...item, parameters } : item)), false),
|
|
127
|
+
/* @__PURE__ */ jsx(
|
|
128
|
+
RowControls,
|
|
129
|
+
{
|
|
130
|
+
label: "action",
|
|
131
|
+
first: index === 0,
|
|
132
|
+
last: index === value.then.length - 1,
|
|
133
|
+
disabled: false,
|
|
134
|
+
onUp: () => {
|
|
135
|
+
const next = [...value.then];
|
|
136
|
+
[next[index - 1], next[index]] = [next[index], next[index - 1]];
|
|
137
|
+
updateActions(next);
|
|
138
|
+
},
|
|
139
|
+
onDown: () => {
|
|
140
|
+
const next = [...value.then];
|
|
141
|
+
[next[index + 1], next[index]] = [next[index], next[index + 1]];
|
|
142
|
+
updateActions(next);
|
|
143
|
+
},
|
|
144
|
+
onRemove: () => updateActions(value.then.filter((item) => item.id !== action.id))
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
] }, action.id);
|
|
148
|
+
}),
|
|
149
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "uix-btn uix-btn--secondary uix-btn--sm", disabled: actions.length === 0, onClick: () => updateActions([...value.then, { id: nextId("action"), type: actions[0]?.id ?? "", parameters: {} }]), children: addActionLabel })
|
|
150
|
+
] }),
|
|
151
|
+
/* @__PURE__ */ jsx("div", { className: "uix-rule-builder__validation", "aria-live": "polite", children: issues.length === 0 ? /* @__PURE__ */ jsx("span", { children: "Rule is valid." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
152
|
+
/* @__PURE__ */ jsxs("strong", { children: [
|
|
153
|
+
issues.length,
|
|
154
|
+
" validation issue",
|
|
155
|
+
issues.length === 1 ? "" : "s"
|
|
156
|
+
] }),
|
|
157
|
+
/* @__PURE__ */ jsx("ul", { children: issues.map((issue, index) => /* @__PURE__ */ jsx("li", { children: issue.message }, `${issue.path}-${index}`)) })
|
|
158
|
+
] }) })
|
|
159
|
+
] });
|
|
160
|
+
}
|
|
161
|
+
export {
|
|
162
|
+
RuleBuilder
|
|
163
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type SchedulingCalendarView = 'month' | 'week' | 'agenda';
|
|
5
|
+
type SchedulingEntryState = 'scheduled' | 'conflicted' | 'in-progress' | 'blackout-violation';
|
|
6
|
+
type SchedulingOverlayKind = 'maintenance' | 'blackout';
|
|
7
|
+
interface SchedulingCalendarEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
start: string;
|
|
11
|
+
end: string;
|
|
12
|
+
state?: SchedulingEntryState;
|
|
13
|
+
meta?: string;
|
|
14
|
+
}
|
|
15
|
+
interface SchedulingCalendarOverlay {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
start: string;
|
|
19
|
+
end: string;
|
|
20
|
+
kind: SchedulingOverlayKind;
|
|
21
|
+
}
|
|
22
|
+
interface SchedulingCalendarProps {
|
|
23
|
+
entries: SchedulingCalendarEntry[];
|
|
24
|
+
anchorDate: string;
|
|
25
|
+
timeZone: string;
|
|
26
|
+
view?: SchedulingCalendarView;
|
|
27
|
+
onViewChange?: (view: SchedulingCalendarView) => void;
|
|
28
|
+
onAnchorDateChange?: (date: string) => void;
|
|
29
|
+
onSelectEntry?: (entry: SchedulingCalendarEntry) => void;
|
|
30
|
+
overlays?: SchedulingCalendarOverlay[];
|
|
31
|
+
filter?: (entry: SchedulingCalendarEntry) => boolean;
|
|
32
|
+
renderEntry?: (entry: SchedulingCalendarEntry) => ReactNode;
|
|
33
|
+
locale?: string;
|
|
34
|
+
loading?: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
onRetry?: () => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Month/week/agenda calendar for UTC ranges rendered in an explicit IANA time zone. */
|
|
40
|
+
declare function SchedulingCalendar({ entries, anchorDate, timeZone, view: controlledView, onViewChange, onAnchorDateChange, onSelectEntry, overlays, filter, renderEntry, locale, loading, error, onRetry, className, }: SchedulingCalendarProps): react.JSX.Element;
|
|
41
|
+
|
|
42
|
+
export { SchedulingCalendar, type SchedulingCalendarEntry, type SchedulingCalendarOverlay, type SchedulingCalendarProps, type SchedulingCalendarView, type SchedulingEntryState, type SchedulingOverlayKind };
|