@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
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,9 @@ __export(src_exports, {
|
|
|
24
24
|
AppShell: () => AppShell,
|
|
25
25
|
Avatar: () => Avatar,
|
|
26
26
|
AvatarGroup: () => AvatarGroup,
|
|
27
|
+
BrandProfileEditor: () => BrandProfileEditor,
|
|
28
|
+
BrandProfiles: () => BrandProfiles,
|
|
29
|
+
BuilderCanvas: () => BuilderCanvas,
|
|
27
30
|
BulkBar: () => BulkBar,
|
|
28
31
|
Button: () => Button,
|
|
29
32
|
ButtonGroup: () => ButtonGroup,
|
|
@@ -31,6 +34,7 @@ __export(src_exports, {
|
|
|
31
34
|
CellStrong: () => CellStrong,
|
|
32
35
|
CellSub: () => CellSub,
|
|
33
36
|
Checkbox: () => Checkbox,
|
|
37
|
+
ColorPicker: () => ColorPicker,
|
|
34
38
|
CommandGroup: () => CommandGroup,
|
|
35
39
|
CommandItem: () => CommandItem,
|
|
36
40
|
CommandPalette: () => CommandPalette,
|
|
@@ -40,9 +44,11 @@ __export(src_exports, {
|
|
|
40
44
|
ComposerBar: () => ComposerBar,
|
|
41
45
|
ContentList: () => ContentList,
|
|
42
46
|
ContentListItem: () => ContentListItem,
|
|
47
|
+
DateRangePicker: () => DateRangePicker,
|
|
43
48
|
DescriptionItem: () => DescriptionItem,
|
|
44
49
|
DescriptionList: () => DescriptionList,
|
|
45
50
|
DetailLayout: () => DetailLayout,
|
|
51
|
+
DiffViewer: () => DiffViewer,
|
|
46
52
|
Drawer: () => Drawer,
|
|
47
53
|
EmptyState: () => EmptyState,
|
|
48
54
|
ErrorState: () => ErrorState,
|
|
@@ -63,11 +69,14 @@ __export(src_exports, {
|
|
|
63
69
|
KanbanCard: () => KanbanCard,
|
|
64
70
|
KanbanColumn: () => KanbanColumn,
|
|
65
71
|
Label: () => Label,
|
|
72
|
+
LicensePositionBar: () => LicensePositionBar,
|
|
66
73
|
List: () => List,
|
|
67
74
|
ListItem: () => ListItem,
|
|
68
75
|
LoadingState: () => LoadingState,
|
|
69
76
|
Mark: () => Mark,
|
|
77
|
+
MatchReview: () => MatchReview,
|
|
70
78
|
Meter: () => Meter,
|
|
79
|
+
MetricInput: () => MetricInput,
|
|
71
80
|
Modal: () => Modal,
|
|
72
81
|
NavFavourites: () => NavFavourites,
|
|
73
82
|
NavGroup: () => NavGroup,
|
|
@@ -84,9 +93,12 @@ __export(src_exports, {
|
|
|
84
93
|
Prose: () => Prose,
|
|
85
94
|
Radio: () => Radio,
|
|
86
95
|
RadioGroup: () => RadioGroup,
|
|
96
|
+
RelationshipGraph: () => RelationshipGraph,
|
|
87
97
|
ResourceGrid: () => ResourceGrid,
|
|
88
98
|
RowAction: () => RowAction,
|
|
89
99
|
RowActions: () => RowActions,
|
|
100
|
+
RuleBuilder: () => RuleBuilder,
|
|
101
|
+
SchedulingCalendar: () => SchedulingCalendar,
|
|
90
102
|
SectionHead: () => SectionHead,
|
|
91
103
|
Segmented: () => Segmented,
|
|
92
104
|
SegmentedOption: () => SegmentedOption,
|
|
@@ -117,31 +129,70 @@ __export(src_exports, {
|
|
|
117
129
|
Tr: () => Tr,
|
|
118
130
|
Tree: () => Tree,
|
|
119
131
|
UserChip: () => UserChip,
|
|
132
|
+
addCalendarDays: () => addCalendarDays,
|
|
133
|
+
addCalendarMonths: () => addCalendarMonths,
|
|
134
|
+
appendRuleNode: () => appendRuleNode,
|
|
135
|
+
applyBrandProfile: () => applyBrandProfile,
|
|
120
136
|
applyFilters: () => applyFilters,
|
|
137
|
+
boundRelationshipGraph: () => boundRelationshipGraph,
|
|
138
|
+
buildMonthGrid: () => buildMonthGrid,
|
|
139
|
+
buildThreeWayDiff: () => buildThreeWayDiff,
|
|
140
|
+
clampMetricValue: () => clampMetricValue,
|
|
121
141
|
clampWidth: () => clampWidth,
|
|
122
142
|
computePosition: () => computePosition,
|
|
143
|
+
contrastRatio: () => contrastRatio,
|
|
123
144
|
cx: () => cx,
|
|
145
|
+
enumerateDateSpan: () => enumerateDateSpan,
|
|
146
|
+
findRuleNodeDepth: () => findRuleNodeDepth,
|
|
124
147
|
flattenTree: () => flattenTree,
|
|
148
|
+
hexToRgb: () => hexToRgb,
|
|
125
149
|
highlightSegments: () => highlightSegments,
|
|
150
|
+
hsvToHex: () => hsvToHex,
|
|
151
|
+
hsvToRgb: () => hsvToRgb,
|
|
152
|
+
isDateInRange: () => isDateInRange,
|
|
153
|
+
isDateUnavailable: () => isDateUnavailable,
|
|
154
|
+
isRuleGroup: () => isRuleGroup,
|
|
155
|
+
layoutRelationshipGraph: () => layoutRelationshipGraph,
|
|
156
|
+
mapRuleGroup: () => mapRuleGroup,
|
|
126
157
|
matchFilter: () => matchFilter,
|
|
158
|
+
meetsContrast: () => meetsContrast,
|
|
127
159
|
mergePinned: () => mergePinned,
|
|
160
|
+
moveRuleNode: () => moveRuleNode,
|
|
128
161
|
multiSort: () => multiSort,
|
|
129
162
|
nextSortDir: () => nextSortDir,
|
|
163
|
+
normalizeHex: () => normalizeHex,
|
|
164
|
+
parseMetricValue: () => parseMetricValue,
|
|
130
165
|
parseView: () => parseView,
|
|
166
|
+
relationshipNeighbors: () => relationshipNeighbors,
|
|
167
|
+
removeRuleNode: () => removeRuleNode,
|
|
131
168
|
reorder: () => reorder,
|
|
169
|
+
restoreBrandProfile: () => restoreBrandProfile,
|
|
170
|
+
rgbToHex: () => rgbToHex,
|
|
171
|
+
rgbToHsv: () => rgbToHsv,
|
|
172
|
+
ruleDepth: () => ruleDepth,
|
|
132
173
|
searchRows: () => searchRows,
|
|
133
174
|
selectAllState: () => selectAllState,
|
|
175
|
+
selectRangeDate: () => selectRangeDate,
|
|
134
176
|
serializeView: () => serializeView,
|
|
135
177
|
shouldVirtualize: () => shouldVirtualize,
|
|
178
|
+
startOfMonth: () => startOfMonth,
|
|
179
|
+
stepMetricValue: () => stepMetricValue,
|
|
180
|
+
summarizeDiff: () => summarizeDiff,
|
|
181
|
+
summarizeRule: () => summarizeRule,
|
|
182
|
+
toDateKey: () => toDateKey,
|
|
136
183
|
toggleId: () => toggleId,
|
|
137
184
|
togglePage: () => togglePage,
|
|
138
185
|
toggleSort: () => toggleSort,
|
|
186
|
+
traverseRelationshipNode: () => traverseRelationshipNode,
|
|
139
187
|
treeNav: () => treeNav,
|
|
140
188
|
useAnchoredPosition: () => useAnchoredPosition,
|
|
141
189
|
useDialog: () => useDialog,
|
|
142
190
|
useTable: () => useTable,
|
|
143
191
|
useVirtualRows: () => useVirtualRows,
|
|
144
|
-
|
|
192
|
+
validateRuleDefinition: () => validateRuleDefinition,
|
|
193
|
+
virtualWindow: () => virtualWindow,
|
|
194
|
+
zonedDateKey: () => zonedDateKey,
|
|
195
|
+
zonedDateSpan: () => zonedDateSpan
|
|
145
196
|
});
|
|
146
197
|
module.exports = __toCommonJS(src_exports);
|
|
147
198
|
|
|
@@ -2549,12 +2600,1664 @@ function StatusRow({ indicator, name, meta, className, ...props }) {
|
|
|
2549
2600
|
meta != null && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "uix-list-meta", children: meta })
|
|
2550
2601
|
] });
|
|
2551
2602
|
}
|
|
2603
|
+
|
|
2604
|
+
// src/rule-builder-model.ts
|
|
2605
|
+
function isRuleGroup(node) {
|
|
2606
|
+
return "conditions" in node;
|
|
2607
|
+
}
|
|
2608
|
+
function ruleDepth(group) {
|
|
2609
|
+
return 1 + Math.max(0, ...group.conditions.filter(isRuleGroup).map(ruleDepth));
|
|
2610
|
+
}
|
|
2611
|
+
function findRuleNodeDepth(group, id, depth = 1) {
|
|
2612
|
+
if (group.id === id) return depth;
|
|
2613
|
+
for (const node of group.conditions) {
|
|
2614
|
+
if (node.id === id) return depth + 1;
|
|
2615
|
+
if (isRuleGroup(node)) {
|
|
2616
|
+
const found = findRuleNodeDepth(node, id, depth + 1);
|
|
2617
|
+
if (found !== void 0) return found;
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
return void 0;
|
|
2621
|
+
}
|
|
2622
|
+
function mapRuleGroup(group, id, update) {
|
|
2623
|
+
if (group.id === id) return update(group);
|
|
2624
|
+
return {
|
|
2625
|
+
...group,
|
|
2626
|
+
conditions: group.conditions.map((node) => {
|
|
2627
|
+
if (node.id === id) return update(node);
|
|
2628
|
+
return isRuleGroup(node) ? mapRuleGroup(node, id, update) : node;
|
|
2629
|
+
})
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
function appendRuleNode(group, parentId, node, maxDepth = 3) {
|
|
2633
|
+
const parentDepth = findRuleNodeDepth(group, parentId);
|
|
2634
|
+
if (parentDepth === void 0) return group;
|
|
2635
|
+
const addedDepth = isRuleGroup(node) ? ruleDepth(node) : 0;
|
|
2636
|
+
if (parentDepth + addedDepth > maxDepth) return group;
|
|
2637
|
+
return mapRuleGroup(group, parentId, (parent) => isRuleGroup(parent) ? { ...parent, conditions: [...parent.conditions, node] } : parent);
|
|
2638
|
+
}
|
|
2639
|
+
function removeRuleNode(group, id) {
|
|
2640
|
+
return {
|
|
2641
|
+
...group,
|
|
2642
|
+
conditions: group.conditions.filter((node) => node.id !== id).map((node) => isRuleGroup(node) ? removeRuleNode(node, id) : node)
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
function moveRuleNode(group, id, direction) {
|
|
2646
|
+
const index = group.conditions.findIndex((node) => node.id === id);
|
|
2647
|
+
if (index >= 0) {
|
|
2648
|
+
const target = index + direction;
|
|
2649
|
+
if (target < 0 || target >= group.conditions.length) return group;
|
|
2650
|
+
const conditions = [...group.conditions];
|
|
2651
|
+
[conditions[index], conditions[target]] = [conditions[target], conditions[index]];
|
|
2652
|
+
return { ...group, conditions };
|
|
2653
|
+
}
|
|
2654
|
+
return {
|
|
2655
|
+
...group,
|
|
2656
|
+
conditions: group.conditions.map((node) => isRuleGroup(node) ? moveRuleNode(node, id, direction) : node)
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
function validateRuleDefinition(value, maxDepth = 3) {
|
|
2660
|
+
const issues = [];
|
|
2661
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2662
|
+
const visit = (group, path, depth) => {
|
|
2663
|
+
if (ids.has(group.id)) issues.push({ path, message: `Duplicate id \u201C${group.id}\u201D.` });
|
|
2664
|
+
ids.add(group.id);
|
|
2665
|
+
if (depth > maxDepth) issues.push({ path, message: `Nesting exceeds ${maxDepth} levels.` });
|
|
2666
|
+
if (group.conditions.length === 0) issues.push({ path, message: "Add at least one condition." });
|
|
2667
|
+
group.conditions.forEach((node, index) => {
|
|
2668
|
+
const nodePath = `${path}.conditions[${index}]`;
|
|
2669
|
+
if (ids.has(node.id)) issues.push({ path: nodePath, message: `Duplicate id \u201C${node.id}\u201D.` });
|
|
2670
|
+
if (isRuleGroup(node)) visit(node, nodePath, depth + 1);
|
|
2671
|
+
else {
|
|
2672
|
+
ids.add(node.id);
|
|
2673
|
+
if (!node.field) issues.push({ path: nodePath, message: "Choose a field." });
|
|
2674
|
+
if (!node.operator) issues.push({ path: nodePath, message: "Choose an operator." });
|
|
2675
|
+
}
|
|
2676
|
+
});
|
|
2677
|
+
};
|
|
2678
|
+
visit(value.when, "when", 1);
|
|
2679
|
+
if (value.then.length === 0) issues.push({ path: "then", message: "Add at least one action." });
|
|
2680
|
+
value.then.forEach((action, index) => {
|
|
2681
|
+
if (ids.has(action.id)) issues.push({ path: `then[${index}]`, message: `Duplicate id \u201C${action.id}\u201D.` });
|
|
2682
|
+
ids.add(action.id);
|
|
2683
|
+
if (!action.type) issues.push({ path: `then[${index}]`, message: "Choose an action." });
|
|
2684
|
+
});
|
|
2685
|
+
return issues;
|
|
2686
|
+
}
|
|
2687
|
+
function summarizeRule(value, labels = {}) {
|
|
2688
|
+
const formatValue = (input) => input === void 0 ? "" : ` ${JSON.stringify(input)}`;
|
|
2689
|
+
const formatGroup = (group) => group.conditions.map(
|
|
2690
|
+
(node) => isRuleGroup(node) ? `(${formatGroup(node)})` : `${labels.fields?.[node.field] ?? node.field} ${labels.operators?.[node.operator] ?? node.operator}${formatValue(node.value)}`
|
|
2691
|
+
).join(` ${group.combinator.toUpperCase()} `);
|
|
2692
|
+
const actions = value.then.map((action) => labels.actions?.[action.type] ?? action.type).join(", ");
|
|
2693
|
+
return `When ${formatGroup(value.when) || "no conditions"}, then ${actions || "no actions"}.`;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
// src/components/RuleBuilder.tsx
|
|
2697
|
+
var import_jsx_runtime46 = require("react/jsx-runtime");
|
|
2698
|
+
var ruleId = 0;
|
|
2699
|
+
var nextId = (prefix) => `${prefix}-${Date.now().toString(36)}-${(++ruleId).toString(36)}`;
|
|
2700
|
+
function defaultCondition(fields, operators) {
|
|
2701
|
+
const field = fields[0];
|
|
2702
|
+
const operator = operators.find((item) => field?.operators?.includes(item.id)) ?? operators[0];
|
|
2703
|
+
return { id: nextId("condition"), field: field?.id ?? "", operator: operator?.id ?? "", value: "" };
|
|
2704
|
+
}
|
|
2705
|
+
function RuleGroupEditor({ group, depth, root, fields, operators, maxDepth, disabled, onRootChange, labels }) {
|
|
2706
|
+
const updateNode = (id, update) => onRootChange(mapRuleGroup(root, id, update));
|
|
2707
|
+
const addCondition = () => onRootChange(appendRuleNode(root, group.id, defaultCondition(fields, operators), maxDepth));
|
|
2708
|
+
const addGroup = () => onRootChange(appendRuleNode(root, group.id, {
|
|
2709
|
+
id: nextId("group"),
|
|
2710
|
+
combinator: "and",
|
|
2711
|
+
conditions: [defaultCondition(fields, operators)]
|
|
2712
|
+
}, maxDepth));
|
|
2713
|
+
return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("fieldset", { className: "uix-rule-builder__group", "data-depth": depth, children: [
|
|
2714
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("legend", { className: "uix-rule-builder__group-heading", children: [
|
|
2715
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { children: depth === 1 ? "When" : "Group" }),
|
|
2716
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("label", { children: [
|
|
2717
|
+
"Match",
|
|
2718
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("select", { className: "uix-select uix-select--sm", value: group.combinator, disabled, onChange: (event) => updateNode(group.id, (node) => ({ ...node, combinator: event.currentTarget.value })), children: [
|
|
2719
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("option", { value: "and", children: "all (AND)" }),
|
|
2720
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("option", { value: "or", children: "any (OR)" })
|
|
2721
|
+
] })
|
|
2722
|
+
] })
|
|
2723
|
+
] }),
|
|
2724
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "uix-rule-builder__rows", children: group.conditions.map((node, index) => isRuleGroup(node) ? /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "uix-rule-builder__nested", children: [
|
|
2725
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)(RuleGroupEditor, { ...{ group: node, depth: depth + 1, root, fields, operators, maxDepth, disabled, onRootChange, labels } }),
|
|
2726
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.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)) })
|
|
2727
|
+
] }, node.id) : /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
|
|
2728
|
+
ConditionRow,
|
|
2729
|
+
{
|
|
2730
|
+
condition: node,
|
|
2731
|
+
fields,
|
|
2732
|
+
operators,
|
|
2733
|
+
disabled,
|
|
2734
|
+
onChange: (condition) => updateNode(node.id, () => condition),
|
|
2735
|
+
controls: /* @__PURE__ */ (0, import_jsx_runtime46.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)) })
|
|
2736
|
+
},
|
|
2737
|
+
node.id
|
|
2738
|
+
)) }),
|
|
2739
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "uix-rule-builder__add", children: [
|
|
2740
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.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 }),
|
|
2741
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: addGroup, disabled: disabled || depth >= maxDepth, children: labels.addGroup })
|
|
2742
|
+
] })
|
|
2743
|
+
] });
|
|
2744
|
+
}
|
|
2745
|
+
function ConditionRow({ condition, fields, operators, disabled, onChange, controls }) {
|
|
2746
|
+
const field = fields.find((item) => item.id === condition.field);
|
|
2747
|
+
const availableOperators = field?.operators?.length ? operators.filter((item) => field.operators.includes(item.id)) : operators;
|
|
2748
|
+
const operator = operators.find((item) => item.id === condition.operator);
|
|
2749
|
+
const setField = (id) => {
|
|
2750
|
+
const nextField = fields.find((item) => item.id === id);
|
|
2751
|
+
const nextOperator = operators.find((item) => nextField?.operators?.includes(item.id)) ?? availableOperators[0] ?? operators[0];
|
|
2752
|
+
onChange({ ...condition, field: id, operator: nextOperator?.id ?? "" });
|
|
2753
|
+
};
|
|
2754
|
+
return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "uix-rule-builder__condition", children: [
|
|
2755
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("label", { children: [
|
|
2756
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "uix-visually-hidden", children: "Field" }),
|
|
2757
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("select", { className: "uix-select", value: condition.field, onChange: (event) => setField(event.currentTarget.value), disabled, children: fields.map((item) => /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("option", { value: item.id, children: item.label }, item.id)) })
|
|
2758
|
+
] }),
|
|
2759
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("label", { children: [
|
|
2760
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "uix-visually-hidden", children: "Operator" }),
|
|
2761
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("select", { className: "uix-select", value: condition.operator, onChange: (event) => onChange({ ...condition, operator: event.currentTarget.value }), disabled, children: availableOperators.map((item) => /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("option", { value: item.id, children: item.label }, item.id)) })
|
|
2762
|
+
] }),
|
|
2763
|
+
operator?.requiresValue !== false && /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "uix-rule-builder__value", children: field?.renderValueEditor?.({ condition, onChange: (value) => onChange({ ...condition, value }), disabled }) ?? /* @__PURE__ */ (0, import_jsx_runtime46.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"}` }) }),
|
|
2764
|
+
controls
|
|
2765
|
+
] });
|
|
2766
|
+
}
|
|
2767
|
+
function RowControls({ label, first, last, disabled, onUp, onDown, onRemove }) {
|
|
2768
|
+
return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("span", { className: "uix-rule-builder__row-actions", children: [
|
|
2769
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.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" }),
|
|
2770
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.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" }),
|
|
2771
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: onRemove, disabled, "aria-label": `Remove ${label}`, children: "Remove" })
|
|
2772
|
+
] });
|
|
2773
|
+
}
|
|
2774
|
+
function RuleBuilder({
|
|
2775
|
+
value,
|
|
2776
|
+
onChange,
|
|
2777
|
+
fields,
|
|
2778
|
+
operators,
|
|
2779
|
+
actions,
|
|
2780
|
+
maxDepth = 3,
|
|
2781
|
+
readOnly = false,
|
|
2782
|
+
validate,
|
|
2783
|
+
className,
|
|
2784
|
+
addConditionLabel = "Add condition",
|
|
2785
|
+
addGroupLabel = "Add group",
|
|
2786
|
+
addActionLabel = "Add action"
|
|
2787
|
+
}) {
|
|
2788
|
+
const issues = [...validateRuleDefinition(value, maxDepth), ...validate?.(value) ?? []];
|
|
2789
|
+
const fieldLabels = Object.fromEntries(fields.map((field) => [field.id, field.label]));
|
|
2790
|
+
const operatorLabels = Object.fromEntries(operators.map((operator) => [operator.id, operator.label]));
|
|
2791
|
+
const actionLabels = Object.fromEntries(actions.map((action) => [action.id, action.label]));
|
|
2792
|
+
if (readOnly) return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("section", { className: cx("uix-rule-builder uix-rule-builder--summary", className), "aria-label": "Rule summary", children: [
|
|
2793
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("p", { children: summarizeRule(value, { fields: fieldLabels, operators: operatorLabels, actions: actionLabels }) }),
|
|
2794
|
+
issues.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("p", { className: "uix-rule-builder__invalid", children: [
|
|
2795
|
+
"Rule has ",
|
|
2796
|
+
issues.length,
|
|
2797
|
+
" validation issue",
|
|
2798
|
+
issues.length === 1 ? "" : "s",
|
|
2799
|
+
"."
|
|
2800
|
+
] })
|
|
2801
|
+
] });
|
|
2802
|
+
const updateActions = (next) => onChange({ ...value, then: next });
|
|
2803
|
+
return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("section", { className: cx("uix-rule-builder", className), "aria-label": "Rule builder", children: [
|
|
2804
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.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 } }),
|
|
2805
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("fieldset", { className: "uix-rule-builder__actions", children: [
|
|
2806
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("legend", { children: "Then" }),
|
|
2807
|
+
value.then.map((action, index) => {
|
|
2808
|
+
const definition = actions.find((item) => item.id === action.type);
|
|
2809
|
+
return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: "uix-rule-builder__action", children: [
|
|
2810
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.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__ */ (0, import_jsx_runtime46.jsx)("option", { value: item.id, children: item.label }, item.id)) }),
|
|
2811
|
+
definition?.renderParameters?.(action, (parameters) => updateActions(value.then.map((item) => item.id === action.id ? { ...item, parameters } : item)), false),
|
|
2812
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
|
|
2813
|
+
RowControls,
|
|
2814
|
+
{
|
|
2815
|
+
label: "action",
|
|
2816
|
+
first: index === 0,
|
|
2817
|
+
last: index === value.then.length - 1,
|
|
2818
|
+
disabled: false,
|
|
2819
|
+
onUp: () => {
|
|
2820
|
+
const next = [...value.then];
|
|
2821
|
+
[next[index - 1], next[index]] = [next[index], next[index - 1]];
|
|
2822
|
+
updateActions(next);
|
|
2823
|
+
},
|
|
2824
|
+
onDown: () => {
|
|
2825
|
+
const next = [...value.then];
|
|
2826
|
+
[next[index + 1], next[index]] = [next[index], next[index + 1]];
|
|
2827
|
+
updateActions(next);
|
|
2828
|
+
},
|
|
2829
|
+
onRemove: () => updateActions(value.then.filter((item) => item.id !== action.id))
|
|
2830
|
+
}
|
|
2831
|
+
)
|
|
2832
|
+
] }, action.id);
|
|
2833
|
+
}),
|
|
2834
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.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 })
|
|
2835
|
+
] }),
|
|
2836
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("div", { className: "uix-rule-builder__validation", "aria-live": "polite", children: issues.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { children: "Rule is valid." }) : /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)(import_jsx_runtime46.Fragment, { children: [
|
|
2837
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("strong", { children: [
|
|
2838
|
+
issues.length,
|
|
2839
|
+
" validation issue",
|
|
2840
|
+
issues.length === 1 ? "" : "s"
|
|
2841
|
+
] }),
|
|
2842
|
+
/* @__PURE__ */ (0, import_jsx_runtime46.jsx)("ul", { children: issues.map((issue, index) => /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("li", { children: issue.message }, `${issue.path}-${index}`)) })
|
|
2843
|
+
] }) })
|
|
2844
|
+
] });
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
// src/components/BuilderCanvas.tsx
|
|
2848
|
+
var import_react23 = require("react");
|
|
2849
|
+
var import_jsx_runtime47 = require("react/jsx-runtime");
|
|
2850
|
+
function BuilderCanvas({
|
|
2851
|
+
items,
|
|
2852
|
+
onItemsChange,
|
|
2853
|
+
palette,
|
|
2854
|
+
selectedId: controlledSelectedId,
|
|
2855
|
+
onSelect,
|
|
2856
|
+
renderItem,
|
|
2857
|
+
renderProperties,
|
|
2858
|
+
loading,
|
|
2859
|
+
error,
|
|
2860
|
+
onRetry,
|
|
2861
|
+
emptyMessage = "Add an item from the palette to begin.",
|
|
2862
|
+
className
|
|
2863
|
+
}) {
|
|
2864
|
+
const [internalSelectedId, setInternalSelectedId] = (0, import_react23.useState)();
|
|
2865
|
+
const [draggedId, setDraggedId] = (0, import_react23.useState)();
|
|
2866
|
+
const propertyHeadingRef = (0, import_react23.useRef)(null);
|
|
2867
|
+
const shouldFocusProperties = (0, import_react23.useRef)(false);
|
|
2868
|
+
const selectedId = controlledSelectedId ?? internalSelectedId;
|
|
2869
|
+
const selected = items.find((item) => item.id === selectedId);
|
|
2870
|
+
(0, import_react23.useEffect)(() => {
|
|
2871
|
+
if (shouldFocusProperties.current && selected) {
|
|
2872
|
+
propertyHeadingRef.current?.focus();
|
|
2873
|
+
shouldFocusProperties.current = false;
|
|
2874
|
+
}
|
|
2875
|
+
}, [selected]);
|
|
2876
|
+
const select = (id, focusProperties = true) => {
|
|
2877
|
+
shouldFocusProperties.current = focusProperties;
|
|
2878
|
+
if (controlledSelectedId === void 0) setInternalSelectedId(id);
|
|
2879
|
+
onSelect?.(id);
|
|
2880
|
+
};
|
|
2881
|
+
const move = (id, direction) => {
|
|
2882
|
+
const index = items.findIndex((item) => item.id === id);
|
|
2883
|
+
const target = index + direction;
|
|
2884
|
+
if (index < 0 || target < 0 || target >= items.length) return;
|
|
2885
|
+
onItemsChange(reorder(items, index, target));
|
|
2886
|
+
};
|
|
2887
|
+
const add = (paletteItem) => {
|
|
2888
|
+
const item = paletteItem.create();
|
|
2889
|
+
onItemsChange([...items, item]);
|
|
2890
|
+
select(item.id);
|
|
2891
|
+
};
|
|
2892
|
+
const remove = (id) => {
|
|
2893
|
+
onItemsChange(items.filter((item) => item.id !== id));
|
|
2894
|
+
if (selectedId === id) select(void 0, false);
|
|
2895
|
+
};
|
|
2896
|
+
const drop = (event, targetId) => {
|
|
2897
|
+
event.preventDefault();
|
|
2898
|
+
if (!draggedId || draggedId === targetId) return;
|
|
2899
|
+
const from = items.findIndex((item) => item.id === draggedId);
|
|
2900
|
+
const to = items.findIndex((item) => item.id === targetId);
|
|
2901
|
+
if (from >= 0 && to >= 0) onItemsChange(reorder(items, from, to));
|
|
2902
|
+
setDraggedId(void 0);
|
|
2903
|
+
};
|
|
2904
|
+
return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("section", { className: cx("uix-builder-canvas", className), "aria-label": "Builder canvas", children: [
|
|
2905
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("aside", { className: "uix-builder-canvas__palette", "aria-label": "Item palette", children: [
|
|
2906
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("h3", { children: "Palette" }),
|
|
2907
|
+
palette.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { children: "No item types available." }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("ul", { children: palette.map((item) => /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("button", { type: "button", onClick: () => add(item), children: [
|
|
2908
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("strong", { children: item.label }),
|
|
2909
|
+
item.description && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { children: item.description }),
|
|
2910
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { "aria-hidden": "true", children: "Add" })
|
|
2911
|
+
] }) }, item.id)) })
|
|
2912
|
+
] }),
|
|
2913
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "uix-builder-canvas__workspace", children: [
|
|
2914
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("h3", { children: "Canvas" }),
|
|
2915
|
+
loading ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("div", { className: "uix-builder-canvas__state", role: "status", children: "Loading canvas\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "uix-builder-canvas__state", role: "alert", children: [
|
|
2916
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { children: error }),
|
|
2917
|
+
onRetry && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: "Try again" })
|
|
2918
|
+
] }) : items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "uix-builder-canvas__state", children: [
|
|
2919
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { children: emptyMessage }),
|
|
2920
|
+
palette[0] && /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("button", { type: "button", className: "uix-btn uix-btn--primary", onClick: () => add(palette[0]), children: [
|
|
2921
|
+
"Add ",
|
|
2922
|
+
palette[0].label
|
|
2923
|
+
] })
|
|
2924
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("ol", { className: "uix-builder-canvas__items", children: items.map((item, index) => /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
|
|
2925
|
+
"li",
|
|
2926
|
+
{
|
|
2927
|
+
draggable: true,
|
|
2928
|
+
onDragStart: () => setDraggedId(item.id),
|
|
2929
|
+
onDragEnd: () => setDraggedId(void 0),
|
|
2930
|
+
onDragOver: (event) => event.preventDefault(),
|
|
2931
|
+
onDrop: (event) => drop(event, item.id),
|
|
2932
|
+
className: cx("uix-builder-canvas__item", selectedId === item.id && "uix-builder-canvas__item--selected"),
|
|
2933
|
+
children: [
|
|
2934
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("button", { type: "button", className: "uix-builder-canvas__item-main", "aria-pressed": selectedId === item.id, onClick: () => select(item.id), children: renderItem?.(item, selectedId === item.id) ?? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(import_jsx_runtime47.Fragment, { children: [
|
|
2935
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("strong", { children: item.label }),
|
|
2936
|
+
item.description && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("span", { children: item.description })
|
|
2937
|
+
] }) }),
|
|
2938
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("span", { className: "uix-builder-canvas__item-actions", children: [
|
|
2939
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("button", { type: "button", onClick: () => move(item.id, -1), disabled: index === 0, "aria-label": `Move ${item.label} up`, children: "Up" }),
|
|
2940
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("button", { type: "button", onClick: () => move(item.id, 1), disabled: index === items.length - 1, "aria-label": `Move ${item.label} down`, children: "Down" }),
|
|
2941
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("button", { type: "button", onClick: () => remove(item.id), "aria-label": `Remove ${item.label}`, children: "Remove" })
|
|
2942
|
+
] })
|
|
2943
|
+
]
|
|
2944
|
+
},
|
|
2945
|
+
item.id
|
|
2946
|
+
)) })
|
|
2947
|
+
] }),
|
|
2948
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("aside", { className: "uix-builder-canvas__properties", "aria-label": "Item properties", children: [
|
|
2949
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("h3", { ref: propertyHeadingRef, tabIndex: -1, children: "Properties" }),
|
|
2950
|
+
!selected ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { children: "Select a canvas item to edit its properties." }) : renderProperties?.(selected, (updated) => onItemsChange(items.map((item) => item.id === updated.id ? updated : item))) ?? /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("dl", { children: [
|
|
2951
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("dt", { children: "Type" }),
|
|
2952
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("dd", { children: selected.type }),
|
|
2953
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("dt", { children: "Label" }),
|
|
2954
|
+
/* @__PURE__ */ (0, import_jsx_runtime47.jsx)("dd", { children: selected.label })
|
|
2955
|
+
] })
|
|
2956
|
+
] })
|
|
2957
|
+
] });
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
// src/components/SchedulingCalendar.tsx
|
|
2961
|
+
var import_react24 = require("react");
|
|
2962
|
+
|
|
2963
|
+
// src/calendar-model.ts
|
|
2964
|
+
function fromDateKey(value) {
|
|
2965
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
|
|
2966
|
+
if (!match) throw new TypeError(`Invalid ISO date: ${value}`);
|
|
2967
|
+
return new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
|
|
2968
|
+
}
|
|
2969
|
+
function toDateKey(date) {
|
|
2970
|
+
return `${date.getUTCFullYear().toString().padStart(4, "0")}-${(date.getUTCMonth() + 1).toString().padStart(2, "0")}-${date.getUTCDate().toString().padStart(2, "0")}`;
|
|
2971
|
+
}
|
|
2972
|
+
function addCalendarDays(value, amount) {
|
|
2973
|
+
const date = fromDateKey(value);
|
|
2974
|
+
date.setUTCDate(date.getUTCDate() + amount);
|
|
2975
|
+
return toDateKey(date);
|
|
2976
|
+
}
|
|
2977
|
+
function addCalendarMonths(value, amount) {
|
|
2978
|
+
const date = fromDateKey(value);
|
|
2979
|
+
const day = date.getUTCDate();
|
|
2980
|
+
date.setUTCDate(1);
|
|
2981
|
+
date.setUTCMonth(date.getUTCMonth() + amount);
|
|
2982
|
+
const last = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)).getUTCDate();
|
|
2983
|
+
date.setUTCDate(Math.min(day, last));
|
|
2984
|
+
return toDateKey(date);
|
|
2985
|
+
}
|
|
2986
|
+
function startOfMonth(value) {
|
|
2987
|
+
return `${value.slice(0, 7)}-01`;
|
|
2988
|
+
}
|
|
2989
|
+
function buildMonthGrid(month, weekStartsOn = 1) {
|
|
2990
|
+
const first = fromDateKey(startOfMonth(month));
|
|
2991
|
+
const offset = (first.getUTCDay() - weekStartsOn + 7) % 7;
|
|
2992
|
+
const start = addCalendarDays(toDateKey(first), -offset);
|
|
2993
|
+
return Array.from({ length: 42 }, (_, index) => {
|
|
2994
|
+
const date = addCalendarDays(start, index);
|
|
2995
|
+
const parsed = fromDateKey(date);
|
|
2996
|
+
return {
|
|
2997
|
+
date,
|
|
2998
|
+
day: parsed.getUTCDate(),
|
|
2999
|
+
inMonth: date.slice(0, 7) === month.slice(0, 7),
|
|
3000
|
+
weekday: parsed.getUTCDay()
|
|
3001
|
+
};
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
function zonedDateKey(instant, timeZone) {
|
|
3005
|
+
const date = instant instanceof Date ? instant : new Date(instant);
|
|
3006
|
+
if (Number.isNaN(date.getTime())) throw new TypeError(`Invalid instant: ${String(instant)}`);
|
|
3007
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
3008
|
+
timeZone,
|
|
3009
|
+
year: "numeric",
|
|
3010
|
+
month: "2-digit",
|
|
3011
|
+
day: "2-digit"
|
|
3012
|
+
}).formatToParts(date);
|
|
3013
|
+
const get = (type) => parts.find((part) => part.type === type)?.value ?? "";
|
|
3014
|
+
return `${get("year")}-${get("month")}-${get("day")}`;
|
|
3015
|
+
}
|
|
3016
|
+
function zonedDateSpan(start, end, timeZone) {
|
|
3017
|
+
const startKey = zonedDateKey(start, timeZone);
|
|
3018
|
+
const endKey = zonedDateKey(end, timeZone);
|
|
3019
|
+
return startKey <= endKey ? { start: startKey, end: endKey } : { start: endKey, end: startKey };
|
|
3020
|
+
}
|
|
3021
|
+
function enumerateDateSpan(span, limit = 370) {
|
|
3022
|
+
const dates = [];
|
|
3023
|
+
let current = span.start;
|
|
3024
|
+
while (current <= span.end && dates.length < limit) {
|
|
3025
|
+
dates.push(current);
|
|
3026
|
+
current = addCalendarDays(current, 1);
|
|
3027
|
+
}
|
|
3028
|
+
return dates;
|
|
3029
|
+
}
|
|
3030
|
+
function selectRangeDate(current, date) {
|
|
3031
|
+
if (!current.start || current.end) return { start: date };
|
|
3032
|
+
return date < current.start ? { start: date, end: current.start } : { start: current.start, end: date };
|
|
3033
|
+
}
|
|
3034
|
+
function isDateInRange(date, value) {
|
|
3035
|
+
return !!value.start && !!value.end && date >= value.start && date <= value.end;
|
|
3036
|
+
}
|
|
3037
|
+
function isDateUnavailable(date, options) {
|
|
3038
|
+
return !!options.min && date < options.min || !!options.max && date > options.max || !!options.isDisabled?.(date);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
// src/components/SchedulingCalendar.tsx
|
|
3042
|
+
var import_jsx_runtime48 = require("react/jsx-runtime");
|
|
3043
|
+
var stateLabel = { scheduled: "Scheduled", conflicted: "Conflicted", "in-progress": "In progress", "blackout-violation": "Blackout violation" };
|
|
3044
|
+
var stateTone = (state) => state === "scheduled" ? "info" : state === "in-progress" ? "success" : "danger";
|
|
3045
|
+
var dateLabel = (date, locale) => new Intl.DateTimeFormat(locale, { dateStyle: "full", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
|
|
3046
|
+
var EMPTY_OVERLAYS = [];
|
|
3047
|
+
function SchedulingCalendar({
|
|
3048
|
+
entries,
|
|
3049
|
+
anchorDate,
|
|
3050
|
+
timeZone,
|
|
3051
|
+
view: controlledView,
|
|
3052
|
+
onViewChange,
|
|
3053
|
+
onAnchorDateChange,
|
|
3054
|
+
onSelectEntry,
|
|
3055
|
+
overlays = EMPTY_OVERLAYS,
|
|
3056
|
+
filter,
|
|
3057
|
+
renderEntry,
|
|
3058
|
+
locale,
|
|
3059
|
+
loading,
|
|
3060
|
+
error,
|
|
3061
|
+
onRetry,
|
|
3062
|
+
className
|
|
3063
|
+
}) {
|
|
3064
|
+
const [internalView, setInternalView] = (0, import_react24.useState)("month");
|
|
3065
|
+
const view = controlledView ?? internalView;
|
|
3066
|
+
const [activeDate, setActiveDate] = (0, import_react24.useState)(anchorDate);
|
|
3067
|
+
const gridRef = (0, import_react24.useRef)(null);
|
|
3068
|
+
const visibleEntries = (0, import_react24.useMemo)(() => entries.filter((entry) => filter?.(entry) ?? true), [entries, filter]);
|
|
3069
|
+
const entrySpans = (0, import_react24.useMemo)(() => new Map(visibleEntries.map((entry) => [entry.id, zonedDateSpan(entry.start, entry.end, timeZone)])), [visibleEntries, timeZone]);
|
|
3070
|
+
const overlaySpans = (0, import_react24.useMemo)(() => new Map(overlays.map((overlay) => [overlay.id, { start: overlay.start.slice(0, 10), end: overlay.end.slice(0, 10) }])), [overlays]);
|
|
3071
|
+
const monthDays = (0, import_react24.useMemo)(() => buildMonthGrid(startOfMonth(anchorDate)), [anchorDate]);
|
|
3072
|
+
const weekday = ((/* @__PURE__ */ new Date(`${anchorDate}T00:00:00Z`)).getUTCDay() + 6) % 7;
|
|
3073
|
+
const weekStart = addCalendarDays(anchorDate, -weekday);
|
|
3074
|
+
const weekDays = (0, import_react24.useMemo)(() => Array.from({ length: 7 }, (_, index) => addCalendarDays(weekStart, index)), [weekStart]);
|
|
3075
|
+
const days = (0, import_react24.useMemo)(() => view === "month" ? monthDays.map((day) => day.date) : weekDays, [view, monthDays, weekDays]);
|
|
3076
|
+
(0, import_react24.useEffect)(() => {
|
|
3077
|
+
if (view !== "agenda" && !days.includes(activeDate)) setActiveDate(days[0]);
|
|
3078
|
+
}, [activeDate, days, view]);
|
|
3079
|
+
const setView = (next) => {
|
|
3080
|
+
if (controlledView === void 0) setInternalView(next);
|
|
3081
|
+
onViewChange?.(next);
|
|
3082
|
+
};
|
|
3083
|
+
const movePeriod = (direction) => onAnchorDateChange?.(view === "month" ? addCalendarMonths(anchorDate, direction) : addCalendarDays(anchorDate, direction * 7));
|
|
3084
|
+
const focusDate = (date) => {
|
|
3085
|
+
setActiveDate(date);
|
|
3086
|
+
requestAnimationFrame(() => gridRef.current?.querySelector(`[data-calendar-date="${date}"]`)?.focus());
|
|
3087
|
+
};
|
|
3088
|
+
const onDayKeyDown = (event, date) => {
|
|
3089
|
+
const offset = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[event.key];
|
|
3090
|
+
if (offset) {
|
|
3091
|
+
event.preventDefault();
|
|
3092
|
+
focusDate(addCalendarDays(date, offset));
|
|
3093
|
+
} else if (event.key === "Home") {
|
|
3094
|
+
event.preventDefault();
|
|
3095
|
+
focusDate(days[0]);
|
|
3096
|
+
} else if (event.key === "End") {
|
|
3097
|
+
event.preventDefault();
|
|
3098
|
+
focusDate(days[days.length - 1]);
|
|
3099
|
+
}
|
|
3100
|
+
};
|
|
3101
|
+
const entriesFor = (date) => visibleEntries.filter((entry) => {
|
|
3102
|
+
const span = entrySpans.get(entry.id);
|
|
3103
|
+
return date >= span.start && date <= span.end;
|
|
3104
|
+
});
|
|
3105
|
+
const overlaysFor = (date) => overlays.filter((overlay) => {
|
|
3106
|
+
const span = overlaySpans.get(overlay.id);
|
|
3107
|
+
return date >= span.start && date <= span.end;
|
|
3108
|
+
});
|
|
3109
|
+
const formatInstant = (instant) => new Intl.DateTimeFormat(locale, { timeZone, dateStyle: "medium", timeStyle: "short" }).format(new Date(instant));
|
|
3110
|
+
return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("section", { className: cx("uix-scheduling-calendar", className), "aria-label": `Scheduling calendar in ${timeZone}`, children: [
|
|
3111
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "uix-scheduling-calendar__header", children: [
|
|
3112
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { children: [
|
|
3113
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: () => movePeriod(-1), disabled: !onAnchorDateChange, children: "Previous" }),
|
|
3114
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", onClick: () => movePeriod(1), disabled: !onAnchorDateChange, children: "Next" })
|
|
3115
|
+
] }),
|
|
3116
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("strong", { children: view === "month" ? new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${anchorDate}T00:00:00Z`)) : `${dateLabel(weekStart, locale)} \u2013 ${dateLabel(weekDays[weekDays.length - 1], locale)}` }),
|
|
3117
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "uix-segmented", role: "group", "aria-label": "Calendar view", children: ["month", "week", "agenda"].map((option) => /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("button", { type: "button", className: "uix-segmented__option", "aria-pressed": view === option, "data-selected": view === option || void 0, onClick: () => setView(option), children: [
|
|
3118
|
+
option[0].toUpperCase(),
|
|
3119
|
+
option.slice(1)
|
|
3120
|
+
] }, option)) })
|
|
3121
|
+
] }),
|
|
3122
|
+
loading ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "uix-scheduling-calendar__state", role: "status", children: "Loading schedule\u2026" }) : error ? /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "uix-scheduling-calendar__state", role: "alert", children: [
|
|
3123
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { children: error }),
|
|
3124
|
+
onRetry && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: "Try again" })
|
|
3125
|
+
] }) : view === "agenda" ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "uix-scheduling-calendar__agenda", role: "region", "aria-label": "Schedule agenda", children: visibleEntries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("p", { children: "No scheduled entries match the current filters." }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("ol", { children: [...visibleEntries].sort((a, b) => a.start.localeCompare(b.start)).map((entry) => {
|
|
3126
|
+
const state = entry.state ?? "scheduled";
|
|
3127
|
+
return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("button", { type: "button", onClick: () => onSelectEntry?.(entry), children: [
|
|
3128
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { children: [
|
|
3129
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("strong", { children: renderEntry?.(entry) ?? entry.title }),
|
|
3130
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { children: [
|
|
3131
|
+
formatInstant(entry.start),
|
|
3132
|
+
" \u2013 ",
|
|
3133
|
+
formatInstant(entry.end)
|
|
3134
|
+
] }),
|
|
3135
|
+
entry.meta && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { children: entry.meta })
|
|
3136
|
+
] }),
|
|
3137
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)(StatusPill, { tone: stateTone(state), children: stateLabel[state] })
|
|
3138
|
+
] }) }, entry.id);
|
|
3139
|
+
}) }) }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { ref: gridRef, className: cx("uix-scheduling-calendar__grid", view === "week" && "uix-scheduling-calendar__grid--week"), role: "group", "aria-label": `${view} schedule`, children: days.map((date, index) => {
|
|
3140
|
+
const dayEntries = entriesFor(date);
|
|
3141
|
+
const dayOverlays = overlaysFor(date);
|
|
3142
|
+
return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "uix-scheduling-calendar__day", "data-outside": view === "month" && !monthDays[index].inMonth || void 0, children: [
|
|
3143
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", className: "uix-scheduling-calendar__date", "data-calendar-date": date, tabIndex: activeDate === date ? 0 : -1, onFocus: () => setActiveDate(date), onKeyDown: (event) => onDayKeyDown(event, date), "aria-label": dateLabel(date, locale), children: Number(date.slice(-2)) }),
|
|
3144
|
+
dayOverlays.map((overlay) => /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "uix-scheduling-calendar__overlay", "data-kind": overlay.kind, children: overlay.label }, overlay.id)),
|
|
3145
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "uix-scheduling-calendar__entries", children: dayEntries.map((entry) => {
|
|
3146
|
+
const span = entrySpans.get(entry.id);
|
|
3147
|
+
const state = entry.state ?? "scheduled";
|
|
3148
|
+
return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("button", { type: "button", className: "uix-scheduling-calendar__entry", "data-state": state, "data-range-start": date === span.start || void 0, "data-range-end": date === span.end || void 0, onClick: () => onSelectEntry?.(entry), "aria-label": `${entry.title}, ${stateLabel[state]}, ${formatInstant(entry.start)} to ${formatInstant(entry.end)}`, children: [
|
|
3149
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { children: renderEntry?.(entry) ?? entry.title }),
|
|
3150
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "uix-visually-hidden", children: stateLabel[state] })
|
|
3151
|
+
] }, entry.id);
|
|
3152
|
+
}) })
|
|
3153
|
+
] }, date);
|
|
3154
|
+
}) }),
|
|
3155
|
+
/* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "uix-scheduling-calendar__legend", "aria-label": "Schedule state legend", children: ["scheduled", "conflicted", "in-progress", "blackout-violation"].map((state) => /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { "data-state": state, children: stateLabel[state] }, state)) })
|
|
3156
|
+
] });
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
// src/components/DateRangePicker.tsx
|
|
3160
|
+
var import_react25 = require("react");
|
|
3161
|
+
var import_jsx_runtime49 = require("react/jsx-runtime");
|
|
3162
|
+
var monthLabel = (date, locale) => new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
|
|
3163
|
+
var dateLabel2 = (date, locale) => new Intl.DateTimeFormat(locale, { dateStyle: "full", timeZone: "UTC" }).format(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
|
|
3164
|
+
function DateRangePicker({
|
|
3165
|
+
value,
|
|
3166
|
+
onChange,
|
|
3167
|
+
visibleMonth: controlledMonth,
|
|
3168
|
+
onVisibleMonthChange,
|
|
3169
|
+
months = 2,
|
|
3170
|
+
min,
|
|
3171
|
+
max,
|
|
3172
|
+
isDateDisabled,
|
|
3173
|
+
locale,
|
|
3174
|
+
label = "Choose date range",
|
|
3175
|
+
invalid,
|
|
3176
|
+
invalidMessage = "The selected date range is invalid.",
|
|
3177
|
+
disabled,
|
|
3178
|
+
className
|
|
3179
|
+
}) {
|
|
3180
|
+
const id = (0, import_react25.useId)();
|
|
3181
|
+
const [internalMonth, setInternalMonth] = (0, import_react25.useState)(() => startOfMonth(controlledMonth ?? value.start ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)));
|
|
3182
|
+
const visibleMonth = startOfMonth(controlledMonth ?? internalMonth);
|
|
3183
|
+
const [activeDate, setActiveDate] = (0, import_react25.useState)(value.end ?? value.start ?? visibleMonth);
|
|
3184
|
+
const rootRef = (0, import_react25.useRef)(null);
|
|
3185
|
+
const pendingFocus = (0, import_react25.useRef)(false);
|
|
3186
|
+
const count = Math.max(1, Math.min(3, months));
|
|
3187
|
+
const calendars = (0, import_react25.useMemo)(() => Array.from({ length: count }, (_, index) => {
|
|
3188
|
+
const month = startOfMonth(addCalendarMonths(visibleMonth, index));
|
|
3189
|
+
return { month, days: buildMonthGrid(month).filter((day) => day.inMonth) };
|
|
3190
|
+
}), [visibleMonth, count]);
|
|
3191
|
+
const rangeInvalid = invalid || !!value.start && !!value.end && value.start > value.end;
|
|
3192
|
+
(0, import_react25.useEffect)(() => {
|
|
3193
|
+
const first = calendars[0]?.days[0]?.date;
|
|
3194
|
+
const finalDays = calendars[calendars.length - 1]?.days;
|
|
3195
|
+
const last = finalDays?.[finalDays.length - 1]?.date;
|
|
3196
|
+
if (first && last && (activeDate < first || activeDate > last)) setActiveDate(first);
|
|
3197
|
+
}, [activeDate, calendars]);
|
|
3198
|
+
(0, import_react25.useEffect)(() => {
|
|
3199
|
+
if (!pendingFocus.current) return;
|
|
3200
|
+
rootRef.current?.querySelector(`[data-date="${activeDate}"]`)?.focus();
|
|
3201
|
+
pendingFocus.current = false;
|
|
3202
|
+
}, [activeDate, visibleMonth]);
|
|
3203
|
+
const setMonth = (month) => {
|
|
3204
|
+
const next = startOfMonth(month);
|
|
3205
|
+
if (controlledMonth === void 0) setInternalMonth(next);
|
|
3206
|
+
onVisibleMonthChange?.(next);
|
|
3207
|
+
};
|
|
3208
|
+
const focusDate = (date) => {
|
|
3209
|
+
setActiveDate(date);
|
|
3210
|
+
if (date < calendars[0].month || date.slice(0, 7) > calendars[calendars.length - 1].month.slice(0, 7)) setMonth(startOfMonth(date));
|
|
3211
|
+
pendingFocus.current = true;
|
|
3212
|
+
};
|
|
3213
|
+
const choose = (date) => {
|
|
3214
|
+
if (disabled || isDateUnavailable(date, { min, max, isDisabled: isDateDisabled })) return;
|
|
3215
|
+
onChange(selectRangeDate(value, date));
|
|
3216
|
+
setActiveDate(date);
|
|
3217
|
+
};
|
|
3218
|
+
const onDateKeyDown = (event, date) => {
|
|
3219
|
+
const offsets = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
|
|
3220
|
+
if (event.key in offsets) {
|
|
3221
|
+
event.preventDefault();
|
|
3222
|
+
focusDate(addCalendarDays(date, offsets[event.key]));
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3225
|
+
if (event.key === "Home" || event.key === "End") {
|
|
3226
|
+
event.preventDefault();
|
|
3227
|
+
const weekday = (/* @__PURE__ */ new Date(`${date}T00:00:00Z`)).getUTCDay();
|
|
3228
|
+
const mondayIndex = (weekday + 6) % 7;
|
|
3229
|
+
focusDate(addCalendarDays(date, event.key === "Home" ? -mondayIndex : 6 - mondayIndex));
|
|
3230
|
+
} else if (event.key === "PageUp" || event.key === "PageDown") {
|
|
3231
|
+
event.preventDefault();
|
|
3232
|
+
focusDate(addCalendarMonths(date, event.key === "PageUp" ? -1 : 1));
|
|
3233
|
+
}
|
|
3234
|
+
};
|
|
3235
|
+
const announcement = !value.start ? "No date range selected." : !value.end ? `Start date ${dateLabel2(value.start, locale)} selected. Choose an end date.` : `${dateLabel2(value.start, locale)} to ${dateLabel2(value.end, locale)} selected.`;
|
|
3236
|
+
return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("section", { className: cx("uix-date-range-picker", rangeInvalid && "uix-date-range-picker--invalid", className), "aria-labelledby": `${id}-label`, children: [
|
|
3237
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "uix-date-range-picker__header", children: [
|
|
3238
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.jsx)("h3", { id: `${id}-label`, children: label }),
|
|
3239
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { children: [
|
|
3240
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.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" }),
|
|
3241
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.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" })
|
|
3242
|
+
] })
|
|
3243
|
+
] }),
|
|
3244
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { ref: rootRef, className: "uix-date-range-picker__months", children: calendars.map(({ month, days }) => /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "uix-date-range-picker__month", children: [
|
|
3245
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.jsx)("h4", { children: monthLabel(month, locale) }),
|
|
3246
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.jsx)("div", { className: "uix-date-range-picker__weekdays", "aria-hidden": "true", children: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((day) => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { children: day }, day)) }),
|
|
3247
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "uix-date-range-picker__grid", role: "group", "aria-label": monthLabel(month, locale), children: [
|
|
3248
|
+
Array.from({ length: ((/* @__PURE__ */ new Date(`${month}T00:00:00Z`)).getUTCDay() + 6) % 7 }, (_, index) => /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("span", { "aria-hidden": "true" }, `blank-${index}`)),
|
|
3249
|
+
days.map((day) => {
|
|
3250
|
+
const unavailable = disabled || isDateUnavailable(day.date, { min, max, isDisabled: isDateDisabled });
|
|
3251
|
+
const edge = day.date === value.start ? "start" : day.date === value.end ? "end" : void 0;
|
|
3252
|
+
return /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
|
|
3253
|
+
"button",
|
|
3254
|
+
{
|
|
3255
|
+
type: "button",
|
|
3256
|
+
"data-date": day.date,
|
|
3257
|
+
"data-range-edge": edge,
|
|
3258
|
+
"data-in-range": isDateInRange(day.date, value) || void 0,
|
|
3259
|
+
"aria-pressed": edge != null || isDateInRange(day.date, value),
|
|
3260
|
+
"aria-label": `${dateLabel2(day.date, locale)}${edge ? `, ${edge} of selected range` : isDateInRange(day.date, value) ? ", in selected range" : ""}`,
|
|
3261
|
+
tabIndex: activeDate === day.date ? 0 : -1,
|
|
3262
|
+
disabled: unavailable,
|
|
3263
|
+
onFocus: () => setActiveDate(day.date),
|
|
3264
|
+
onKeyDown: (event) => onDateKeyDown(event, day.date),
|
|
3265
|
+
onClick: () => choose(day.date),
|
|
3266
|
+
children: day.day
|
|
3267
|
+
},
|
|
3268
|
+
day.date
|
|
3269
|
+
);
|
|
3270
|
+
})
|
|
3271
|
+
] })
|
|
3272
|
+
] }, month)) }),
|
|
3273
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: "uix-date-range-picker__selection", "aria-live": "polite", children: announcement }),
|
|
3274
|
+
rangeInvalid && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)("p", { className: "uix-field__error", role: "alert", children: invalidMessage })
|
|
3275
|
+
] });
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// src/components/RelationshipGraph.tsx
|
|
3279
|
+
var import_react26 = require("react");
|
|
3280
|
+
|
|
3281
|
+
// src/relationship-graph-model.ts
|
|
3282
|
+
function boundRelationshipGraph(nodes, edges, maxNodes = 40) {
|
|
3283
|
+
const boundedNodes = nodes.slice(0, Math.max(1, maxNodes));
|
|
3284
|
+
const ids = new Set(boundedNodes.map((node) => node.id));
|
|
3285
|
+
return {
|
|
3286
|
+
nodes: boundedNodes,
|
|
3287
|
+
edges: edges.filter((edge) => ids.has(edge.source) && ids.has(edge.target)),
|
|
3288
|
+
omittedNodeCount: Math.max(0, nodes.length - boundedNodes.length)
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
function relationshipNeighbors(nodeId, edges) {
|
|
3292
|
+
const result = [];
|
|
3293
|
+
for (const edge of edges) {
|
|
3294
|
+
if (edge.source === nodeId) result.push(edge.target);
|
|
3295
|
+
else if (edge.target === nodeId) result.push(edge.source);
|
|
3296
|
+
}
|
|
3297
|
+
return [...new Set(result)];
|
|
3298
|
+
}
|
|
3299
|
+
function layoutRelationshipGraph(nodes, edges, rootId = nodes[0]?.id) {
|
|
3300
|
+
if (nodes.length === 0) return [];
|
|
3301
|
+
const root = nodes.some((node) => node.id === rootId) ? rootId : nodes[0].id;
|
|
3302
|
+
const depth = /* @__PURE__ */ new Map([[root, 0]]);
|
|
3303
|
+
const queue = [root];
|
|
3304
|
+
while (queue.length) {
|
|
3305
|
+
const current = queue.shift();
|
|
3306
|
+
for (const neighbor of relationshipNeighbors(current, edges)) {
|
|
3307
|
+
if (!depth.has(neighbor) && nodes.some((node) => node.id === neighbor)) {
|
|
3308
|
+
depth.set(neighbor, (depth.get(current) ?? 0) + 1);
|
|
3309
|
+
queue.push(neighbor);
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
let disconnectedDepth = Math.max(0, ...depth.values()) + 1;
|
|
3314
|
+
nodes.forEach((node) => {
|
|
3315
|
+
if (!depth.has(node.id)) depth.set(node.id, disconnectedDepth);
|
|
3316
|
+
});
|
|
3317
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3318
|
+
nodes.forEach((node) => {
|
|
3319
|
+
const level = depth.get(node.id);
|
|
3320
|
+
groups.set(level, [...groups.get(level) ?? [], node]);
|
|
3321
|
+
});
|
|
3322
|
+
const maxDepth = Math.max(1, ...groups.keys());
|
|
3323
|
+
return nodes.map((node) => {
|
|
3324
|
+
const level = depth.get(node.id);
|
|
3325
|
+
if (node.x !== void 0 && node.y !== void 0) return { ...node, x: node.x, y: node.y, depth: level };
|
|
3326
|
+
if (level === 0) return { ...node, x: 50, y: 50, depth: 0 };
|
|
3327
|
+
const peers = groups.get(level);
|
|
3328
|
+
const index = peers.findIndex((peer) => peer.id === node.id);
|
|
3329
|
+
const angle = Math.PI * 2 * index / peers.length - Math.PI / 2;
|
|
3330
|
+
const radius = 16 + level / maxDepth * 26;
|
|
3331
|
+
return {
|
|
3332
|
+
...node,
|
|
3333
|
+
x: Number((50 + Math.cos(angle) * radius).toFixed(3)),
|
|
3334
|
+
y: Number((50 + Math.sin(angle) * radius).toFixed(3)),
|
|
3335
|
+
depth: level
|
|
3336
|
+
};
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
function traverseRelationshipNode(nodes, currentId, direction) {
|
|
3340
|
+
if (nodes.length === 0) return void 0;
|
|
3341
|
+
const current = Math.max(0, nodes.findIndex((node) => node.id === currentId));
|
|
3342
|
+
return nodes[(current + direction + nodes.length) % nodes.length]?.id;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
// src/components/RelationshipGraph.tsx
|
|
3346
|
+
var import_jsx_runtime50 = require("react/jsx-runtime");
|
|
3347
|
+
var EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
3348
|
+
var EMPTY_LEGEND = [];
|
|
3349
|
+
function RelationshipGraph({
|
|
3350
|
+
nodes,
|
|
3351
|
+
edges,
|
|
3352
|
+
selectedId,
|
|
3353
|
+
onSelect,
|
|
3354
|
+
onExpandNeighbors,
|
|
3355
|
+
onOpenDetails,
|
|
3356
|
+
maxNodes = 40,
|
|
3357
|
+
density = "standard",
|
|
3358
|
+
highlightedNodeIds = EMPTY_IDS,
|
|
3359
|
+
conflictedNodeIds = EMPTY_IDS,
|
|
3360
|
+
highlightedEdgeIds = EMPTY_IDS,
|
|
3361
|
+
conflictedEdgeIds = EMPTY_IDS,
|
|
3362
|
+
legend = EMPTY_LEGEND,
|
|
3363
|
+
loading,
|
|
3364
|
+
error,
|
|
3365
|
+
onRetry,
|
|
3366
|
+
className
|
|
3367
|
+
}) {
|
|
3368
|
+
const bounded = (0, import_react26.useMemo)(() => boundRelationshipGraph(nodes, edges, maxNodes), [nodes, edges, maxNodes]);
|
|
3369
|
+
const positioned = (0, import_react26.useMemo)(() => layoutRelationshipGraph(bounded.nodes, bounded.edges, selectedId), [bounded, selectedId]);
|
|
3370
|
+
const positionedById = (0, import_react26.useMemo)(() => new Map(positioned.map((node) => [node.id, node])), [positioned]);
|
|
3371
|
+
const [zoom, setZoom] = (0, import_react26.useState)(1);
|
|
3372
|
+
const [pan, setPan] = (0, import_react26.useState)({ x: 0, y: 0 });
|
|
3373
|
+
const [focusedId, setFocusedId] = (0, import_react26.useState)(selectedId ?? positioned[0]?.id);
|
|
3374
|
+
const svgRef = (0, import_react26.useRef)(null);
|
|
3375
|
+
const select = (id) => {
|
|
3376
|
+
const node = bounded.nodes.find((item) => item.id === id);
|
|
3377
|
+
if (node) onSelect?.(node);
|
|
3378
|
+
};
|
|
3379
|
+
const focusNode = (id) => {
|
|
3380
|
+
if (!id) return;
|
|
3381
|
+
setFocusedId(id);
|
|
3382
|
+
requestAnimationFrame(() => svgRef.current?.querySelector(`[data-node-id="${id}"]`)?.focus());
|
|
3383
|
+
};
|
|
3384
|
+
const onNodeKeyDown = (event, id) => {
|
|
3385
|
+
if (["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"].includes(event.key)) {
|
|
3386
|
+
event.preventDefault();
|
|
3387
|
+
focusNode(traverseRelationshipNode(positioned, id, event.key === "ArrowRight" || event.key === "ArrowDown" ? 1 : -1));
|
|
3388
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
3389
|
+
event.preventDefault();
|
|
3390
|
+
select(id);
|
|
3391
|
+
}
|
|
3392
|
+
};
|
|
3393
|
+
const reset = () => {
|
|
3394
|
+
setZoom(1);
|
|
3395
|
+
setPan({ x: 0, y: 0 });
|
|
3396
|
+
};
|
|
3397
|
+
if (loading) return /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: cx("uix-relationship-graph uix-relationship-graph--state", className), role: "status", children: "Loading relationships\u2026" });
|
|
3398
|
+
if (error) return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: cx("uix-relationship-graph uix-relationship-graph--state", className), role: "alert", children: [
|
|
3399
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("p", { children: error }),
|
|
3400
|
+
onRetry && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: "Try again" })
|
|
3401
|
+
] });
|
|
3402
|
+
if (bounded.nodes.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: cx("uix-relationship-graph uix-relationship-graph--state", className), children: [
|
|
3403
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("p", { children: "No relationships to display." }),
|
|
3404
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("p", { children: "Adjust the neighborhood or filters to include a node." })
|
|
3405
|
+
] });
|
|
3406
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("section", { className: cx("uix-relationship-graph", `uix-relationship-graph--${density}`, className), "aria-label": "Relationship graph", children: [
|
|
3407
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "uix-relationship-graph__toolbar", "aria-label": "Graph view controls", children: [
|
|
3408
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => setPan((value) => ({ ...value, x: value.x - 24 })), "aria-label": "Pan left", children: "Left" }),
|
|
3409
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => setPan((value) => ({ ...value, y: value.y - 24 })), "aria-label": "Pan up", children: "Up" }),
|
|
3410
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => setPan((value) => ({ ...value, y: value.y + 24 })), "aria-label": "Pan down", children: "Down" }),
|
|
3411
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => setPan((value) => ({ ...value, x: value.x + 24 })), "aria-label": "Pan right", children: "Right" }),
|
|
3412
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => setZoom((value) => Math.min(2, Number((value + 0.2).toFixed(1)))), "aria-label": "Zoom in", children: "Zoom in" }),
|
|
3413
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => setZoom((value) => Math.max(0.5, Number((value - 0.2).toFixed(1)))), "aria-label": "Zoom out", children: "Zoom out" }),
|
|
3414
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: reset, children: "Reset view" }),
|
|
3415
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("span", { "aria-live": "polite", children: [
|
|
3416
|
+
"Zoom ",
|
|
3417
|
+
Math.round(zoom * 100),
|
|
3418
|
+
"%"
|
|
3419
|
+
] })
|
|
3420
|
+
] }),
|
|
3421
|
+
bounded.omittedNodeCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("p", { className: "uix-relationship-graph__bounded", role: "status", children: [
|
|
3422
|
+
"Showing ",
|
|
3423
|
+
bounded.nodes.length,
|
|
3424
|
+
" nodes; ",
|
|
3425
|
+
bounded.omittedNodeCount,
|
|
3426
|
+
" outside this bounded neighborhood."
|
|
3427
|
+
] }),
|
|
3428
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("div", { className: "uix-relationship-graph__visual", children: /* @__PURE__ */ (0, import_jsx_runtime50.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__ */ (0, import_jsx_runtime50.jsxs)("g", { transform: `translate(${pan.x} ${pan.y}) scale(${zoom})`, children: [
|
|
3429
|
+
bounded.edges.map((edge) => {
|
|
3430
|
+
const source = positionedById.get(edge.source);
|
|
3431
|
+
const target = positionedById.get(edge.target);
|
|
3432
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.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__ */ (0, import_jsx_runtime50.jsx)("title", { children: edge.label ?? edge.type ?? "Relationship" }) }, edge.id);
|
|
3433
|
+
}),
|
|
3434
|
+
positioned.map((node) => /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)(
|
|
3435
|
+
"g",
|
|
3436
|
+
{
|
|
3437
|
+
transform: `translate(${node.x * 6} ${node.y * 4})`,
|
|
3438
|
+
role: "button",
|
|
3439
|
+
"aria-label": `${node.label}${node.type ? `, ${node.type}` : ""}${conflictedNodeIds.has(node.id) ? ", conflict" : ""}`,
|
|
3440
|
+
tabIndex: focusedId === node.id ? 0 : -1,
|
|
3441
|
+
"data-node-id": node.id,
|
|
3442
|
+
className: "uix-relationship-graph__node",
|
|
3443
|
+
"data-selected": selectedId === node.id || void 0,
|
|
3444
|
+
"data-highlighted": highlightedNodeIds.has(node.id) || void 0,
|
|
3445
|
+
"data-conflicted": conflictedNodeIds.has(node.id) || void 0,
|
|
3446
|
+
onFocus: () => setFocusedId(node.id),
|
|
3447
|
+
onKeyDown: (event) => onNodeKeyDown(event, node.id),
|
|
3448
|
+
onClick: () => select(node.id),
|
|
3449
|
+
children: [
|
|
3450
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("rect", { x: "-55", y: "-18", width: "110", height: "36", rx: "8" }),
|
|
3451
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("text", { textAnchor: "middle", dominantBaseline: "middle", children: node.label.length > 16 ? `${node.label.slice(0, 15)}\u2026` : node.label })
|
|
3452
|
+
]
|
|
3453
|
+
},
|
|
3454
|
+
node.id
|
|
3455
|
+
))
|
|
3456
|
+
] }) }) }),
|
|
3457
|
+
legend.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("ul", { className: "uix-relationship-graph__legend", "aria-label": "Relationship types", children: legend.map((item) => /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("li", { "data-type": item.id, children: item.label }, item.id)) }),
|
|
3458
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("div", { className: "uix-relationship-graph__list", role: "region", "aria-label": "Accessible relationship list", children: [
|
|
3459
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("h3", { children: "Relationship list" }),
|
|
3460
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("ul", { children: bounded.nodes.map((node) => {
|
|
3461
|
+
const neighbors = relationshipNeighbors(node.id, bounded.edges).map((id) => bounded.nodes.find((item) => item.id === id)?.label ?? id);
|
|
3462
|
+
return /* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("li", { "data-selected": selectedId === node.id || void 0, children: [
|
|
3463
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("button", { type: "button", onClick: () => select(node.id), "aria-pressed": selectedId === node.id, children: [
|
|
3464
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("strong", { children: node.label }),
|
|
3465
|
+
node.type && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { children: node.type }),
|
|
3466
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsx)("span", { children: neighbors.length ? `Connected to ${neighbors.join(", ")}` : "No visible connections" })
|
|
3467
|
+
] }),
|
|
3468
|
+
/* @__PURE__ */ (0, import_jsx_runtime50.jsxs)("span", { className: "uix-relationship-graph__list-actions", children: [
|
|
3469
|
+
onExpandNeighbors && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => onExpandNeighbors(node), children: "Expand neighbors" }),
|
|
3470
|
+
onOpenDetails && /* @__PURE__ */ (0, import_jsx_runtime50.jsx)("button", { type: "button", onClick: () => onOpenDetails(node), children: "Open details" })
|
|
3471
|
+
] })
|
|
3472
|
+
] }, node.id);
|
|
3473
|
+
}) })
|
|
3474
|
+
] })
|
|
3475
|
+
] });
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
// src/components/MatchReview.tsx
|
|
3479
|
+
var import_react27 = require("react");
|
|
3480
|
+
var import_jsx_runtime51 = require("react/jsx-runtime");
|
|
3481
|
+
var valueText = (value) => value == null ? "\u2014" : typeof value === "string" ? value : JSON.stringify(value);
|
|
3482
|
+
function MatchReview({
|
|
3483
|
+
incoming,
|
|
3484
|
+
incomingLabel = "Incoming record",
|
|
3485
|
+
fields,
|
|
3486
|
+
candidates,
|
|
3487
|
+
selectedIds,
|
|
3488
|
+
onSelectionChange,
|
|
3489
|
+
onDecision,
|
|
3490
|
+
onBulkDecision,
|
|
3491
|
+
loading,
|
|
3492
|
+
error,
|
|
3493
|
+
onRetry,
|
|
3494
|
+
className
|
|
3495
|
+
}) {
|
|
3496
|
+
const [internalSelection, setInternalSelection] = (0, import_react27.useState)(/* @__PURE__ */ new Set());
|
|
3497
|
+
const [announcement, setAnnouncement] = (0, import_react27.useState)("");
|
|
3498
|
+
const [bulkPending, setBulkPending] = (0, import_react27.useState)(false);
|
|
3499
|
+
const selection = selectedIds ?? internalSelection;
|
|
3500
|
+
const pending = (0, import_react27.useMemo)(() => candidates.filter((candidate) => candidate.status !== "resolved"), [candidates]);
|
|
3501
|
+
const setSelection = (next) => {
|
|
3502
|
+
if (selectedIds === void 0) setInternalSelection(next);
|
|
3503
|
+
onSelectionChange?.(next);
|
|
3504
|
+
};
|
|
3505
|
+
const toggle = (id) => {
|
|
3506
|
+
const next = new Set(selection);
|
|
3507
|
+
if (next.has(id)) next.delete(id);
|
|
3508
|
+
else next.add(id);
|
|
3509
|
+
setSelection(next);
|
|
3510
|
+
};
|
|
3511
|
+
const runBulk = async (action) => {
|
|
3512
|
+
if (!onBulkDecision || selection.size === 0 || bulkPending) return;
|
|
3513
|
+
const ids = [...selection];
|
|
3514
|
+
setBulkPending(true);
|
|
3515
|
+
setAnnouncement(`${action === "accept" ? "Accepting" : "Dismissing"} ${ids.length} candidate${ids.length === 1 ? "" : "s"}\u2026`);
|
|
3516
|
+
try {
|
|
3517
|
+
const result = await onBulkDecision(ids, action);
|
|
3518
|
+
const succeeded = result?.succeeded ?? ids.length;
|
|
3519
|
+
const failed = result?.failed ?? 0;
|
|
3520
|
+
setAnnouncement(`${succeeded} candidate${succeeded === 1 ? "" : "s"} ${action === "accept" ? "accepted" : "dismissed"}${failed ? `; ${failed} failed` : ""}.`);
|
|
3521
|
+
if (!failed) setSelection(/* @__PURE__ */ new Set());
|
|
3522
|
+
} catch {
|
|
3523
|
+
setAnnouncement(`Bulk ${action} failed. Try again.`);
|
|
3524
|
+
} finally {
|
|
3525
|
+
setBulkPending(false);
|
|
3526
|
+
}
|
|
3527
|
+
};
|
|
3528
|
+
if (loading) return /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: cx("uix-match-review uix-match-review--state", className), role: "status", children: "Loading match candidates\u2026" });
|
|
3529
|
+
if (error) return /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { className: cx("uix-match-review uix-match-review--state", className), role: "alert", children: [
|
|
3530
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("p", { children: error }),
|
|
3531
|
+
onRetry && /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("button", { type: "button", className: "uix-btn uix-btn--secondary", onClick: onRetry, children: "Try again" })
|
|
3532
|
+
] });
|
|
3533
|
+
if (candidates.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { className: cx("uix-match-review uix-match-review--state", className), children: [
|
|
3534
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("p", { children: "No candidate matches found." }),
|
|
3535
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("p", { children: "Choose another record source or continue without a match." })
|
|
3536
|
+
] });
|
|
3537
|
+
return /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("section", { className: cx("uix-match-review", className), "aria-label": "Match review", children: [
|
|
3538
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { className: "uix-match-review__summary", children: [
|
|
3539
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { children: [
|
|
3540
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("strong", { children: incomingLabel }),
|
|
3541
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("span", { children: [
|
|
3542
|
+
pending.length,
|
|
3543
|
+
" pending of ",
|
|
3544
|
+
candidates.length
|
|
3545
|
+
] })
|
|
3546
|
+
] }),
|
|
3547
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "uix-match-review__incoming", children: fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("span", { children: [
|
|
3548
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("b", { children: field.label }),
|
|
3549
|
+
field.render?.(incoming[field.id], incoming) ?? valueText(incoming[field.id])
|
|
3550
|
+
] }, field.id)) })
|
|
3551
|
+
] }),
|
|
3552
|
+
onBulkDecision && /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("div", { className: "uix-match-review__bulk", "aria-label": "Bulk candidate actions", children: [
|
|
3553
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("span", { children: [
|
|
3554
|
+
selection.size,
|
|
3555
|
+
" selected"
|
|
3556
|
+
] }),
|
|
3557
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.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" }),
|
|
3558
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.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" })
|
|
3559
|
+
] }),
|
|
3560
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "uix-match-review__table-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("table", { className: "uix-table uix-match-review__table", children: [
|
|
3561
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("caption", { className: "uix-visually-hidden", children: "Candidate matches and available decisions" }),
|
|
3562
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("tr", { children: [
|
|
3563
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("th", { scope: "col", children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("span", { className: "uix-visually-hidden", children: "Select" }) }),
|
|
3564
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("th", { scope: "col", children: "Confidence" }),
|
|
3565
|
+
fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("th", { scope: "col", children: field.label }, field.id)),
|
|
3566
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("th", { scope: "col", children: "Status" }),
|
|
3567
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("th", { scope: "col", children: "Actions" })
|
|
3568
|
+
] }) }),
|
|
3569
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("tbody", { children: candidates.map((candidate) => {
|
|
3570
|
+
const resolved = candidate.status === "resolved";
|
|
3571
|
+
return /* @__PURE__ */ (0, import_jsx_runtime51.jsxs)("tr", { "data-status": candidate.status ?? "pending", children: [
|
|
3572
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("input", { type: "checkbox", checked: selection.has(candidate.id), onChange: () => toggle(candidate.id), "aria-label": `Select candidate ${candidate.id}` }) }),
|
|
3573
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("span", { className: "uix-match-review__confidence", children: candidate.confidence == null ? "Not scored" : `${Math.round(candidate.confidence * 100)}%` }) }),
|
|
3574
|
+
fields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("td", { children: field.render?.(candidate.record[field.id], candidate.record) ?? valueText(candidate.record[field.id]) }, field.id)),
|
|
3575
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(StatusPill, { tone: resolved ? "success" : "warning", children: resolved ? candidate.decision ?? "Resolved" : "Pending" }) }),
|
|
3576
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime51.jsx)("div", { className: "uix-match-review__actions", children: ["accept", "dismiss", "split", "pick-other"].map((action) => /* @__PURE__ */ (0, import_jsx_runtime51.jsx)(
|
|
3577
|
+
"button",
|
|
3578
|
+
{
|
|
3579
|
+
type: "button",
|
|
3580
|
+
className: "uix-btn uix-btn--ghost uix-btn--sm",
|
|
3581
|
+
disabled: resolved,
|
|
3582
|
+
onClick: () => onDecision(candidate.id, action),
|
|
3583
|
+
children: action === "pick-other" ? "Pick other" : `${action[0].toUpperCase()}${action.slice(1)}`
|
|
3584
|
+
},
|
|
3585
|
+
action
|
|
3586
|
+
)) }) })
|
|
3587
|
+
] }, candidate.id);
|
|
3588
|
+
}) })
|
|
3589
|
+
] }) }),
|
|
3590
|
+
/* @__PURE__ */ (0, import_jsx_runtime51.jsx)("p", { className: "uix-visually-hidden", "aria-live": "polite", children: announcement })
|
|
3591
|
+
] });
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
// src/components/MetricInput.tsx
|
|
3595
|
+
var import_react28 = require("react");
|
|
3596
|
+
|
|
3597
|
+
// src/metric-model.ts
|
|
3598
|
+
function decimals(value) {
|
|
3599
|
+
const text = String(value).toLowerCase();
|
|
3600
|
+
if (text.includes("e-")) return Number(text.split("e-")[1]);
|
|
3601
|
+
return text.includes(".") ? text.split(".")[1].length : 0;
|
|
3602
|
+
}
|
|
3603
|
+
function clampMetricValue(value, min, max) {
|
|
3604
|
+
return Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min ?? Number.NEGATIVE_INFINITY, value));
|
|
3605
|
+
}
|
|
3606
|
+
function stepMetricValue(value, direction, options = {}) {
|
|
3607
|
+
const step = options.step && options.step > 0 ? options.step : 1;
|
|
3608
|
+
const base = value ?? options.min ?? 0;
|
|
3609
|
+
const precision = Math.max(decimals(base), decimals(step), decimals(options.min ?? 0), decimals(options.max ?? 0));
|
|
3610
|
+
const next = Number((base + direction * step).toFixed(precision));
|
|
3611
|
+
return clampMetricValue(next, options.min, options.max);
|
|
3612
|
+
}
|
|
3613
|
+
function parseMetricValue(value) {
|
|
3614
|
+
if (value.trim() === "") return null;
|
|
3615
|
+
const parsed = Number(value);
|
|
3616
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
// src/components/MetricInput.tsx
|
|
3620
|
+
var import_jsx_runtime52 = require("react/jsx-runtime");
|
|
3621
|
+
var defaultMetricFormat = (input) => input == null ? "" : String(input);
|
|
3622
|
+
function MetricInput({
|
|
3623
|
+
value,
|
|
3624
|
+
onValueChange,
|
|
3625
|
+
min,
|
|
3626
|
+
max,
|
|
3627
|
+
step = 1,
|
|
3628
|
+
unit,
|
|
3629
|
+
label,
|
|
3630
|
+
invalid,
|
|
3631
|
+
errorMessage,
|
|
3632
|
+
disabled,
|
|
3633
|
+
readOnly,
|
|
3634
|
+
parse = parseMetricValue,
|
|
3635
|
+
format = defaultMetricFormat,
|
|
3636
|
+
className,
|
|
3637
|
+
id: providedId
|
|
3638
|
+
}) {
|
|
3639
|
+
const generatedId = (0, import_react28.useId)();
|
|
3640
|
+
const id = providedId ?? generatedId;
|
|
3641
|
+
const errorId = `${id}-error`;
|
|
3642
|
+
const [text, setText] = (0, import_react28.useState)(() => format(value));
|
|
3643
|
+
(0, import_react28.useEffect)(() => setText(format(value)), [value, format]);
|
|
3644
|
+
const commit = (input) => {
|
|
3645
|
+
const parsed = parse(input);
|
|
3646
|
+
if (input.trim() === "" || parsed !== null) {
|
|
3647
|
+
onValueChange(parsed === null ? null : clampMetricValue(parsed, min, max));
|
|
3648
|
+
}
|
|
3649
|
+
};
|
|
3650
|
+
const stepBy = (direction) => {
|
|
3651
|
+
const next = stepMetricValue(value, direction, { min, max, step });
|
|
3652
|
+
onValueChange(next);
|
|
3653
|
+
setText(format(next));
|
|
3654
|
+
};
|
|
3655
|
+
const onKeyDown = (event) => {
|
|
3656
|
+
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
|
|
3657
|
+
event.preventDefault();
|
|
3658
|
+
stepBy(event.key === "ArrowUp" ? 1 : -1);
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
const onChange = (event) => {
|
|
3662
|
+
setText(event.currentTarget.value);
|
|
3663
|
+
commit(event.currentTarget.value);
|
|
3664
|
+
};
|
|
3665
|
+
const onBlur = (event) => {
|
|
3666
|
+
const parsed = parse(event.currentTarget.value);
|
|
3667
|
+
setText(parsed === null && event.currentTarget.value.trim() !== "" ? format(value) : format(parsed));
|
|
3668
|
+
};
|
|
3669
|
+
return /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: cx("uix-metric-input", invalid && "uix-metric-input--invalid", className), children: [
|
|
3670
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsx)("label", { className: "uix-field__label", htmlFor: id, children: label }),
|
|
3671
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("div", { className: "uix-metric-input__control", children: [
|
|
3672
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsx)(
|
|
3673
|
+
Input,
|
|
3674
|
+
{
|
|
3675
|
+
id,
|
|
3676
|
+
value: text,
|
|
3677
|
+
onChange,
|
|
3678
|
+
onBlur,
|
|
3679
|
+
onKeyDown,
|
|
3680
|
+
inputMode: "decimal",
|
|
3681
|
+
role: "spinbutton",
|
|
3682
|
+
"aria-valuenow": value ?? void 0,
|
|
3683
|
+
"aria-valuemin": min,
|
|
3684
|
+
"aria-valuemax": max,
|
|
3685
|
+
"aria-describedby": invalid && errorMessage ? errorId : void 0,
|
|
3686
|
+
invalid,
|
|
3687
|
+
disabled,
|
|
3688
|
+
readOnly
|
|
3689
|
+
}
|
|
3690
|
+
),
|
|
3691
|
+
unit && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("span", { className: "uix-metric-input__unit", "aria-hidden": "true", children: unit }),
|
|
3692
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("span", { className: "uix-metric-input__steps", children: [
|
|
3693
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsx)("button", { type: "button", className: "uix-metric-input__step", onClick: () => stepBy(1), disabled: disabled || readOnly, "aria-label": `Increase ${label}`, children: "+" }),
|
|
3694
|
+
/* @__PURE__ */ (0, import_jsx_runtime52.jsx)("button", { type: "button", className: "uix-metric-input__step", onClick: () => stepBy(-1), disabled: disabled || readOnly, "aria-label": `Decrease ${label}`, children: "\u2212" })
|
|
3695
|
+
] })
|
|
3696
|
+
] }),
|
|
3697
|
+
unit && /* @__PURE__ */ (0, import_jsx_runtime52.jsxs)("span", { className: "uix-visually-hidden", children: [
|
|
3698
|
+
"Unit: ",
|
|
3699
|
+
unit
|
|
3700
|
+
] }),
|
|
3701
|
+
invalid && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime52.jsx)("span", { id: errorId, className: "uix-field__error", children: errorMessage })
|
|
3702
|
+
] });
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
// src/components/LicensePositionBar.tsx
|
|
3706
|
+
var import_jsx_runtime53 = require("react/jsx-runtime");
|
|
3707
|
+
var defaultCapacityFormat = (value) => new Intl.NumberFormat().format(value);
|
|
3708
|
+
function LicensePositionBar({
|
|
3709
|
+
consumed,
|
|
3710
|
+
entitled,
|
|
3711
|
+
consumedLabel = "Consumed",
|
|
3712
|
+
entitledLabel = "Entitled",
|
|
3713
|
+
availableLabel = "available",
|
|
3714
|
+
overLabel = "over limit",
|
|
3715
|
+
unit = "",
|
|
3716
|
+
formatValue = defaultCapacityFormat,
|
|
3717
|
+
className,
|
|
3718
|
+
...props
|
|
3719
|
+
}) {
|
|
3720
|
+
const safeConsumed = Math.max(0, consumed);
|
|
3721
|
+
const safeEntitled = Math.max(0, entitled);
|
|
3722
|
+
const over = Math.max(0, safeConsumed - safeEntitled);
|
|
3723
|
+
const available = Math.max(0, safeEntitled - safeConsumed);
|
|
3724
|
+
const percent = safeEntitled === 0 ? safeConsumed > 0 ? 100 : 0 : safeConsumed / safeEntitled * 100;
|
|
3725
|
+
const format = (value) => `${formatValue(value)}${unit ? ` ${unit}` : ""}`;
|
|
3726
|
+
const summary = `${consumedLabel}: ${format(safeConsumed)}. ${entitledLabel}: ${format(safeEntitled)}. ${over > 0 ? `${format(over)} ${overLabel}` : `${format(available)} ${availableLabel}`}.`;
|
|
3727
|
+
return /* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: cx("uix-license-position", over > 0 && "uix-license-position--over", className), ...props, children: [
|
|
3728
|
+
/* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "uix-license-position__header", children: [
|
|
3729
|
+
/* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("span", { className: "uix-license-position__value", children: [
|
|
3730
|
+
format(safeConsumed),
|
|
3731
|
+
" / ",
|
|
3732
|
+
format(safeEntitled)
|
|
3733
|
+
] }),
|
|
3734
|
+
/* @__PURE__ */ (0, import_jsx_runtime53.jsx)(StatusPill, { tone: over > 0 ? "danger" : percent >= 80 ? "warning" : "success", children: over > 0 ? `${format(over)} ${overLabel}` : `${format(available)} ${availableLabel}` })
|
|
3735
|
+
] }),
|
|
3736
|
+
/* @__PURE__ */ (0, import_jsx_runtime53.jsxs)("div", { className: "uix-license-position__track", children: [
|
|
3737
|
+
/* @__PURE__ */ (0, import_jsx_runtime53.jsx)(Meter, { value: Math.min(100, percent), tone: over > 0 ? "danger" : percent >= 80 ? "warning" : "success", "aria-label": summary, "aria-valuetext": summary }),
|
|
3738
|
+
over > 0 && /* @__PURE__ */ (0, import_jsx_runtime53.jsx)(
|
|
3739
|
+
"span",
|
|
3740
|
+
{
|
|
3741
|
+
className: "uix-license-position__overflow",
|
|
3742
|
+
style: { width: `${Math.min(100, safeEntitled === 0 ? 100 : over / safeEntitled * 100)}%` },
|
|
3743
|
+
"aria-hidden": "true"
|
|
3744
|
+
}
|
|
3745
|
+
)
|
|
3746
|
+
] }),
|
|
3747
|
+
/* @__PURE__ */ (0, import_jsx_runtime53.jsx)("span", { className: "uix-visually-hidden", children: summary })
|
|
3748
|
+
] });
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
// src/components/BrandProfiles.tsx
|
|
3752
|
+
var import_react30 = require("react");
|
|
3753
|
+
|
|
3754
|
+
// src/color-model.ts
|
|
3755
|
+
var clamp2 = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
3756
|
+
function normalizeHex(value) {
|
|
3757
|
+
const raw = value.trim().replace(/^#/, "");
|
|
3758
|
+
if (/^[\da-f]{3}$/i.test(raw)) return `#${raw.split("").map((char) => char + char).join("").toUpperCase()}`;
|
|
3759
|
+
if (/^[\da-f]{6}$/i.test(raw)) return `#${raw.toUpperCase()}`;
|
|
3760
|
+
return void 0;
|
|
3761
|
+
}
|
|
3762
|
+
function hexToRgb(value) {
|
|
3763
|
+
const hex = normalizeHex(value);
|
|
3764
|
+
if (!hex) return void 0;
|
|
3765
|
+
return {
|
|
3766
|
+
r: Number.parseInt(hex.slice(1, 3), 16),
|
|
3767
|
+
g: Number.parseInt(hex.slice(3, 5), 16),
|
|
3768
|
+
b: Number.parseInt(hex.slice(5, 7), 16)
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
function rgbToHex({ r, g, b }) {
|
|
3772
|
+
return `#${[r, g, b].map((value) => Math.round(clamp2(value, 0, 255)).toString(16).padStart(2, "0")).join("").toUpperCase()}`;
|
|
3773
|
+
}
|
|
3774
|
+
function rgbToHsv({ r, g, b }) {
|
|
3775
|
+
const values = [r, g, b].map((value) => clamp2(value, 0, 255) / 255);
|
|
3776
|
+
const max = Math.max(...values);
|
|
3777
|
+
const min = Math.min(...values);
|
|
3778
|
+
const delta = max - min;
|
|
3779
|
+
let h = 0;
|
|
3780
|
+
if (delta !== 0) {
|
|
3781
|
+
if (max === values[0]) h = 60 * ((values[1] - values[2]) / delta % 6);
|
|
3782
|
+
else if (max === values[1]) h = 60 * ((values[2] - values[0]) / delta + 2);
|
|
3783
|
+
else h = 60 * ((values[0] - values[1]) / delta + 4);
|
|
3784
|
+
}
|
|
3785
|
+
if (h < 0) h += 360;
|
|
3786
|
+
return { h, s: max === 0 ? 0 : delta / max, v: max };
|
|
3787
|
+
}
|
|
3788
|
+
function hsvToRgb({ h, s, v }) {
|
|
3789
|
+
const hue = (h % 360 + 360) % 360;
|
|
3790
|
+
const sat = clamp2(s, 0, 1);
|
|
3791
|
+
const value = clamp2(v, 0, 1);
|
|
3792
|
+
const chroma = value * sat;
|
|
3793
|
+
const x = chroma * (1 - Math.abs(hue / 60 % 2 - 1));
|
|
3794
|
+
const m = value - chroma;
|
|
3795
|
+
const sector = Math.floor(hue / 60);
|
|
3796
|
+
const rgb = sector === 0 ? [chroma, x, 0] : sector === 1 ? [x, chroma, 0] : sector === 2 ? [0, chroma, x] : sector === 3 ? [0, x, chroma] : sector === 4 ? [x, 0, chroma] : [chroma, 0, x];
|
|
3797
|
+
return { r: (rgb[0] + m) * 255, g: (rgb[1] + m) * 255, b: (rgb[2] + m) * 255 };
|
|
3798
|
+
}
|
|
3799
|
+
function hsvToHex(value) {
|
|
3800
|
+
return rgbToHex(hsvToRgb(value));
|
|
3801
|
+
}
|
|
3802
|
+
function relativeLuminance({ r, g, b }) {
|
|
3803
|
+
const linear = [r, g, b].map((channel) => {
|
|
3804
|
+
const value = channel / 255;
|
|
3805
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
3806
|
+
});
|
|
3807
|
+
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
|
|
3808
|
+
}
|
|
3809
|
+
function contrastRatio(background, foreground) {
|
|
3810
|
+
const bg = hexToRgb(background);
|
|
3811
|
+
const fg = hexToRgb(foreground);
|
|
3812
|
+
if (!bg || !fg) return void 0;
|
|
3813
|
+
const [light, dark] = [relativeLuminance(bg), relativeLuminance(fg)].sort((a, b) => b - a);
|
|
3814
|
+
return (light + 0.05) / (dark + 0.05);
|
|
3815
|
+
}
|
|
3816
|
+
function meetsContrast(background, foreground, minimum = 4.5) {
|
|
3817
|
+
return (contrastRatio(background, foreground) ?? 0) >= minimum;
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3820
|
+
// src/components/ColorPicker.tsx
|
|
3821
|
+
var import_react29 = require("react");
|
|
3822
|
+
var import_jsx_runtime54 = require("react/jsx-runtime");
|
|
3823
|
+
var EMPTY_COLORS = [];
|
|
3824
|
+
function ColorPicker({
|
|
3825
|
+
value,
|
|
3826
|
+
onChange,
|
|
3827
|
+
foreground,
|
|
3828
|
+
presets = EMPTY_COLORS,
|
|
3829
|
+
recent = EMPTY_COLORS,
|
|
3830
|
+
minimumContrast = 4.5,
|
|
3831
|
+
label = "Choose color",
|
|
3832
|
+
disabled,
|
|
3833
|
+
className
|
|
3834
|
+
}) {
|
|
3835
|
+
const id = (0, import_react29.useId)();
|
|
3836
|
+
const triggerRef = (0, import_react29.useRef)(null);
|
|
3837
|
+
const dialogRef = (0, import_react29.useRef)(null);
|
|
3838
|
+
const [open, setOpen] = (0, import_react29.useState)(false);
|
|
3839
|
+
const normalized = normalizeHex(value) ?? "#000000";
|
|
3840
|
+
const [hexText, setHexText] = (0, import_react29.useState)(normalized);
|
|
3841
|
+
const hsv = (0, import_react29.useMemo)(() => rgbToHsv(hexToRgb(normalized)), [normalized]);
|
|
3842
|
+
const ratio = contrastRatio(normalized, foreground) ?? 0;
|
|
3843
|
+
const passes = ratio >= minimumContrast;
|
|
3844
|
+
(0, import_react29.useEffect)(() => setHexText(normalized), [normalized]);
|
|
3845
|
+
(0, import_react29.useEffect)(() => {
|
|
3846
|
+
if (!open) return;
|
|
3847
|
+
const dialog = dialogRef.current;
|
|
3848
|
+
dialog?.querySelector("input,button")?.focus();
|
|
3849
|
+
const onPointer = (event) => {
|
|
3850
|
+
if (!dialog?.contains(event.target) && !triggerRef.current?.contains(event.target)) setOpen(false);
|
|
3851
|
+
};
|
|
3852
|
+
document.addEventListener("mousedown", onPointer);
|
|
3853
|
+
return () => document.removeEventListener("mousedown", onPointer);
|
|
3854
|
+
}, [open]);
|
|
3855
|
+
const close = () => {
|
|
3856
|
+
setOpen(false);
|
|
3857
|
+
requestAnimationFrame(() => triggerRef.current?.focus());
|
|
3858
|
+
};
|
|
3859
|
+
const onDialogKeyDown = (event) => {
|
|
3860
|
+
if (event.key === "Escape") {
|
|
3861
|
+
event.preventDefault();
|
|
3862
|
+
close();
|
|
3863
|
+
return;
|
|
3864
|
+
}
|
|
3865
|
+
if (event.key !== "Tab") return;
|
|
3866
|
+
const focusable = Array.from(dialogRef.current?.querySelectorAll('button:not([disabled]),input:not([disabled]),[tabindex="0"]') ?? []);
|
|
3867
|
+
if (!focusable.length) return;
|
|
3868
|
+
const first = focusable[0];
|
|
3869
|
+
const last = focusable[focusable.length - 1];
|
|
3870
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
3871
|
+
event.preventDefault();
|
|
3872
|
+
last.focus();
|
|
3873
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
3874
|
+
event.preventDefault();
|
|
3875
|
+
first.focus();
|
|
3876
|
+
}
|
|
3877
|
+
};
|
|
3878
|
+
const updateHsv = (part) => onChange(hsvToHex({ ...hsv, ...part }));
|
|
3879
|
+
const commitHex = () => {
|
|
3880
|
+
const next = normalizeHex(hexText);
|
|
3881
|
+
if (next) onChange(next);
|
|
3882
|
+
};
|
|
3883
|
+
const choose = (candidate) => {
|
|
3884
|
+
const next = normalizeHex(candidate);
|
|
3885
|
+
if (next) onChange(next);
|
|
3886
|
+
};
|
|
3887
|
+
const sampleStyle = { backgroundColor: normalized };
|
|
3888
|
+
return /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: cx("uix-color-picker", className), children: [
|
|
3889
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
|
|
3890
|
+
"button",
|
|
3891
|
+
{
|
|
3892
|
+
ref: triggerRef,
|
|
3893
|
+
type: "button",
|
|
3894
|
+
className: "uix-color-picker__trigger",
|
|
3895
|
+
"aria-label": `${label}: ${normalized}`,
|
|
3896
|
+
"aria-expanded": open,
|
|
3897
|
+
"aria-controls": `${id}-dialog`,
|
|
3898
|
+
onClick: () => setOpen((current) => !current),
|
|
3899
|
+
disabled,
|
|
3900
|
+
children: [
|
|
3901
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", { className: "uix-color-picker__swatch", style: sampleStyle, "aria-hidden": "true" }),
|
|
3902
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", { children: normalized })
|
|
3903
|
+
]
|
|
3904
|
+
}
|
|
3905
|
+
),
|
|
3906
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)(
|
|
3907
|
+
"div",
|
|
3908
|
+
{
|
|
3909
|
+
ref: dialogRef,
|
|
3910
|
+
id: `${id}-dialog`,
|
|
3911
|
+
role: "dialog",
|
|
3912
|
+
"aria-modal": "true",
|
|
3913
|
+
"aria-label": label,
|
|
3914
|
+
className: "uix-color-picker__popover",
|
|
3915
|
+
onKeyDown: onDialogKeyDown,
|
|
3916
|
+
children: [
|
|
3917
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("div", { className: "uix-color-picker__preview", style: sampleStyle, children: /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("span", { style: { color: foreground }, children: "Aa" }) }),
|
|
3918
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("label", { className: "uix-color-picker__range", children: [
|
|
3919
|
+
"Hue ",
|
|
3920
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("span", { children: [
|
|
3921
|
+
Math.round(hsv.h),
|
|
3922
|
+
"\xB0"
|
|
3923
|
+
] }),
|
|
3924
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("input", { type: "range", min: "0", max: "359", value: Math.round(hsv.h), onChange: (event) => updateHsv({ h: Number(event.currentTarget.value) }) })
|
|
3925
|
+
] }),
|
|
3926
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("label", { className: "uix-color-picker__range", children: [
|
|
3927
|
+
"Saturation ",
|
|
3928
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("span", { children: [
|
|
3929
|
+
Math.round(hsv.s * 100),
|
|
3930
|
+
"%"
|
|
3931
|
+
] }),
|
|
3932
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("input", { type: "range", min: "0", max: "100", value: Math.round(hsv.s * 100), onChange: (event) => updateHsv({ s: Number(event.currentTarget.value) / 100 }) })
|
|
3933
|
+
] }),
|
|
3934
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("label", { className: "uix-color-picker__range", children: [
|
|
3935
|
+
"Brightness ",
|
|
3936
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("span", { children: [
|
|
3937
|
+
Math.round(hsv.v * 100),
|
|
3938
|
+
"%"
|
|
3939
|
+
] }),
|
|
3940
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("input", { type: "range", min: "0", max: "100", value: Math.round(hsv.v * 100), onChange: (event) => updateHsv({ v: Number(event.currentTarget.value) / 100 }) })
|
|
3941
|
+
] }),
|
|
3942
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("label", { className: "uix-color-picker__hex", children: [
|
|
3943
|
+
"Hex",
|
|
3944
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("span", { className: "uix-color-picker__hex-row", children: [
|
|
3945
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("input", { className: "uix-input", value: hexText, onChange: (event) => setHexText(event.currentTarget.value), onBlur: commitHex, onKeyDown: (event) => {
|
|
3946
|
+
if (event.key === "Enter") commitHex();
|
|
3947
|
+
}, "aria-invalid": !normalizeHex(hexText) }),
|
|
3948
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("button", { type: "button", className: "uix-btn uix-btn--secondary uix-btn--sm", onClick: commitHex, disabled: !normalizeHex(hexText), children: "Set" })
|
|
3949
|
+
] })
|
|
3950
|
+
] }),
|
|
3951
|
+
(presets.length > 0 || recent.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("div", { className: "uix-color-picker__palettes", children: [
|
|
3952
|
+
presets.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("fieldset", { children: [
|
|
3953
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("legend", { children: "Presets" }),
|
|
3954
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("div", { children: presets.map((color) => /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("button", { type: "button", className: "uix-color-picker__preset", style: { backgroundColor: normalizeHex(color) ?? color }, onClick: () => choose(color), "aria-label": `Use preset ${color}` }, color)) })
|
|
3955
|
+
] }),
|
|
3956
|
+
recent.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime54.jsxs)("fieldset", { children: [
|
|
3957
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("legend", { children: "Recent" }),
|
|
3958
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("div", { children: recent.map((color) => /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("button", { type: "button", className: "uix-color-picker__preset", style: { backgroundColor: normalizeHex(color) ?? color }, onClick: () => choose(color), "aria-label": `Use recent color ${color}` }, color)) })
|
|
3959
|
+
] })
|
|
3960
|
+
] }),
|
|
3961
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.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: [
|
|
3962
|
+
"Contrast ",
|
|
3963
|
+
ratio.toFixed(2),
|
|
3964
|
+
":1 \u2014 ",
|
|
3965
|
+
passes ? `passes WCAG AA (${minimumContrast}:1)` : `does not meet WCAG AA (${minimumContrast}:1)`
|
|
3966
|
+
] }),
|
|
3967
|
+
/* @__PURE__ */ (0, import_jsx_runtime54.jsx)("button", { type: "button", className: "uix-btn uix-btn--primary uix-btn--sm", onClick: close, children: "Done" })
|
|
3968
|
+
]
|
|
3969
|
+
}
|
|
3970
|
+
)
|
|
3971
|
+
] });
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
// src/components/BrandProfiles.tsx
|
|
3975
|
+
var import_jsx_runtime55 = require("react/jsx-runtime");
|
|
3976
|
+
var PROFILE_PROPERTIES = {
|
|
3977
|
+
brand: "--uix-brand",
|
|
3978
|
+
brandForeground: "--uix-brand-fg",
|
|
3979
|
+
sans: "--uix-font-sans",
|
|
3980
|
+
heading: "--uix-font-heading",
|
|
3981
|
+
mono: "--uix-font-mono"
|
|
3982
|
+
};
|
|
3983
|
+
var EMPTY_PROFILES = [];
|
|
3984
|
+
var EMPTY_COLORS2 = [];
|
|
3985
|
+
function applyBrandProfile(profile, target) {
|
|
3986
|
+
const element = target ?? (typeof document === "undefined" ? void 0 : document.documentElement);
|
|
3987
|
+
if (!element) return void 0;
|
|
3988
|
+
const brand = normalizeHex(profile.brand);
|
|
3989
|
+
const foreground = normalizeHex(profile.brandForeground);
|
|
3990
|
+
if (!brand || !foreground) throw new TypeError("Brand profiles require valid hex brand and foreground colors.");
|
|
3991
|
+
const properties = {};
|
|
3992
|
+
Object.values(PROFILE_PROPERTIES).forEach((property) => {
|
|
3993
|
+
properties[property] = element.style.getPropertyValue(property);
|
|
3994
|
+
});
|
|
3995
|
+
const snapshot = {
|
|
3996
|
+
profileId: element.dataset.uixBrandProfile,
|
|
3997
|
+
properties,
|
|
3998
|
+
logoSrc: element.dataset.uixBrandLogoSrc,
|
|
3999
|
+
logoAlt: element.dataset.uixBrandLogoAlt
|
|
4000
|
+
};
|
|
4001
|
+
element.style.setProperty(PROFILE_PROPERTIES.brand, brand);
|
|
4002
|
+
element.style.setProperty(PROFILE_PROPERTIES.brandForeground, foreground);
|
|
4003
|
+
const typography = profile.typography ?? {};
|
|
4004
|
+
for (const [key, property] of Object.entries({ sans: PROFILE_PROPERTIES.sans, heading: PROFILE_PROPERTIES.heading, mono: PROFILE_PROPERTIES.mono })) {
|
|
4005
|
+
const value = typography[key];
|
|
4006
|
+
if (value) element.style.setProperty(property, value);
|
|
4007
|
+
else element.style.removeProperty(property);
|
|
4008
|
+
}
|
|
4009
|
+
element.dataset.uixBrandProfile = profile.id;
|
|
4010
|
+
if (profile.logo) {
|
|
4011
|
+
element.dataset.uixBrandLogoSrc = profile.logo.src;
|
|
4012
|
+
element.dataset.uixBrandLogoAlt = profile.logo.alt;
|
|
4013
|
+
} else {
|
|
4014
|
+
delete element.dataset.uixBrandLogoSrc;
|
|
4015
|
+
delete element.dataset.uixBrandLogoAlt;
|
|
4016
|
+
}
|
|
4017
|
+
return snapshot;
|
|
4018
|
+
}
|
|
4019
|
+
function restoreBrandProfile(snapshot, target) {
|
|
4020
|
+
const element = target ?? (typeof document === "undefined" ? void 0 : document.documentElement);
|
|
4021
|
+
if (!element) return;
|
|
4022
|
+
Object.entries(snapshot.properties).forEach(([property, value]) => {
|
|
4023
|
+
if (value) element.style.setProperty(property, value);
|
|
4024
|
+
else element.style.removeProperty(property);
|
|
4025
|
+
});
|
|
4026
|
+
if (snapshot.profileId) element.dataset.uixBrandProfile = snapshot.profileId;
|
|
4027
|
+
else delete element.dataset.uixBrandProfile;
|
|
4028
|
+
if (snapshot.logoSrc) element.dataset.uixBrandLogoSrc = snapshot.logoSrc;
|
|
4029
|
+
else delete element.dataset.uixBrandLogoSrc;
|
|
4030
|
+
if (snapshot.logoAlt) element.dataset.uixBrandLogoAlt = snapshot.logoAlt;
|
|
4031
|
+
else delete element.dataset.uixBrandLogoAlt;
|
|
4032
|
+
}
|
|
4033
|
+
function BrandProfileEditor({
|
|
4034
|
+
value,
|
|
4035
|
+
onChange,
|
|
4036
|
+
profiles = EMPTY_PROFILES,
|
|
4037
|
+
onSelectProfile,
|
|
4038
|
+
onApply,
|
|
4039
|
+
applyTarget,
|
|
4040
|
+
onLogoFile,
|
|
4041
|
+
colorPresets = EMPTY_COLORS2,
|
|
4042
|
+
className
|
|
4043
|
+
}) {
|
|
4044
|
+
const [announcement, setAnnouncement] = (0, import_react30.useState)("");
|
|
4045
|
+
const update = (patch) => onChange({ ...value, ...patch });
|
|
4046
|
+
const apply = () => {
|
|
4047
|
+
if (applyTarget !== null) applyBrandProfile(value, applyTarget);
|
|
4048
|
+
onApply?.(value);
|
|
4049
|
+
setAnnouncement(`${value.name} applied.`);
|
|
4050
|
+
};
|
|
4051
|
+
const onFile = (event) => {
|
|
4052
|
+
const file = event.currentTarget.files?.[0];
|
|
4053
|
+
if (file) onLogoFile?.(file);
|
|
4054
|
+
};
|
|
4055
|
+
const previewStyle = {
|
|
4056
|
+
"--uix-brand": normalizeHex(value.brand) ?? value.brand,
|
|
4057
|
+
"--uix-brand-fg": normalizeHex(value.brandForeground) ?? value.brandForeground,
|
|
4058
|
+
...value.typography?.sans ? { "--uix-font-sans": value.typography.sans } : {},
|
|
4059
|
+
...value.typography?.heading ? { "--uix-font-heading": value.typography.heading } : {}
|
|
4060
|
+
};
|
|
4061
|
+
return /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("section", { className: cx("uix-brand-profiles", className), "aria-label": "Brand profile editor", children: [
|
|
4062
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: "uix-brand-profiles__form", children: [
|
|
4063
|
+
profiles.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("label", { className: "uix-field__label", children: [
|
|
4064
|
+
"Profile",
|
|
4065
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("select", { className: "uix-select", value: value.id, onChange: (event) => {
|
|
4066
|
+
const selected = profiles.find((profile) => profile.id === event.currentTarget.value);
|
|
4067
|
+
if (selected) onSelectProfile?.(selected);
|
|
4068
|
+
}, children: profiles.map((profile) => /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("option", { value: profile.id, children: profile.name }, profile.id)) })
|
|
4069
|
+
] }),
|
|
4070
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("label", { className: "uix-field__label", children: [
|
|
4071
|
+
"Name",
|
|
4072
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("input", { className: "uix-input", value: value.name, onChange: (event) => update({ name: event.currentTarget.value }) })
|
|
4073
|
+
] }),
|
|
4074
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: "uix-brand-profiles__colors", children: [
|
|
4075
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("label", { children: [
|
|
4076
|
+
"Brand",
|
|
4077
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)(ColorPicker, { label: "Brand color", value: value.brand, foreground: value.brandForeground, presets: colorPresets, onChange: (brand) => update({ brand }) })
|
|
4078
|
+
] }),
|
|
4079
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("label", { children: [
|
|
4080
|
+
"On brand",
|
|
4081
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)(ColorPicker, { label: "Brand foreground color", value: value.brandForeground, foreground: value.brand, presets: colorPresets, onChange: (brandForeground) => update({ brandForeground }) })
|
|
4082
|
+
] })
|
|
4083
|
+
] }),
|
|
4084
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("label", { className: "uix-field__label", children: [
|
|
4085
|
+
"Body font",
|
|
4086
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("input", { className: "uix-input", value: value.typography?.sans ?? "", onChange: (event) => update({ typography: { ...value.typography, sans: event.currentTarget.value || void 0 } }) })
|
|
4087
|
+
] }),
|
|
4088
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("label", { className: "uix-field__label", children: [
|
|
4089
|
+
"Heading font",
|
|
4090
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("input", { className: "uix-input", value: value.typography?.heading ?? "", onChange: (event) => update({ typography: { ...value.typography, heading: event.currentTarget.value || void 0 } }) })
|
|
4091
|
+
] }),
|
|
4092
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("label", { className: "uix-file-upload uix-brand-profiles__upload", children: [
|
|
4093
|
+
"Logo file",
|
|
4094
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("input", { type: "file", accept: "image/*", onChange: onFile })
|
|
4095
|
+
] }),
|
|
4096
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("button", { type: "button", className: "uix-btn uix-btn--primary", onClick: apply, children: "Apply profile" }),
|
|
4097
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("span", { className: "uix-visually-hidden", "aria-live": "polite", children: announcement })
|
|
4098
|
+
] }),
|
|
4099
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: "uix-brand-profiles__preview", style: previewStyle, "aria-label": `Live preview of ${value.name}`, children: [
|
|
4100
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: "uix-brand-profiles__preview-header", children: [
|
|
4101
|
+
value.logo ? /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("img", { src: value.logo.src, alt: value.logo.alt }) : /* @__PURE__ */ (0, import_jsx_runtime55.jsx)("span", { className: "uix-brand-profiles__logo-placeholder", children: "Logo" }),
|
|
4102
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("strong", { children: value.name || "Brand preview" })
|
|
4103
|
+
] }),
|
|
4104
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsxs)("div", { className: "uix-brand-profiles__preview-body", children: [
|
|
4105
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("h3", { children: "Readable, serializable branding" }),
|
|
4106
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("p", { children: "Accent, links, rings, and muted states continue through the existing UIx token chain." }),
|
|
4107
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("button", { type: "button", className: "uix-btn uix-btn--primary", children: "Primary action" }),
|
|
4108
|
+
/* @__PURE__ */ (0, import_jsx_runtime55.jsx)("a", { href: "#brand-profile-preview", children: "Preview link" })
|
|
4109
|
+
] })
|
|
4110
|
+
] })
|
|
4111
|
+
] });
|
|
4112
|
+
}
|
|
4113
|
+
var BrandProfiles = BrandProfileEditor;
|
|
4114
|
+
|
|
4115
|
+
// src/components/DiffViewer.tsx
|
|
4116
|
+
var import_react31 = require("react");
|
|
4117
|
+
|
|
4118
|
+
// src/diff-model.ts
|
|
4119
|
+
function isJsonObject(value) {
|
|
4120
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4121
|
+
}
|
|
4122
|
+
function jsonEqual(a, b) {
|
|
4123
|
+
if (a === b) return true;
|
|
4124
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
4125
|
+
return a.length === b.length && a.every((value, index) => jsonEqual(value, b[index]));
|
|
4126
|
+
}
|
|
4127
|
+
if (isJsonObject(a) && isJsonObject(b)) {
|
|
4128
|
+
const aKeys = Object.keys(a).sort();
|
|
4129
|
+
const bKeys = Object.keys(b).sort();
|
|
4130
|
+
return aKeys.length === bKeys.length && aKeys.every((key, index) => key === bKeys[index] && jsonEqual(a[key], b[key]));
|
|
4131
|
+
}
|
|
4132
|
+
return false;
|
|
4133
|
+
}
|
|
4134
|
+
function classify(base, current, incoming) {
|
|
4135
|
+
if (base === void 0) return "added";
|
|
4136
|
+
if (incoming === void 0) return "removed";
|
|
4137
|
+
const currentChanged = !jsonEqual(base, current);
|
|
4138
|
+
const incomingChanged = !jsonEqual(base, incoming);
|
|
4139
|
+
if (currentChanged && incomingChanged && !jsonEqual(current, incoming)) return "conflicted";
|
|
4140
|
+
return "changed";
|
|
4141
|
+
}
|
|
4142
|
+
function buildThreeWayDiff(base, current, incoming, path = "$") {
|
|
4143
|
+
if (jsonEqual(current, incoming)) return [];
|
|
4144
|
+
if (isJsonObject(base) || isJsonObject(current) || isJsonObject(incoming)) {
|
|
4145
|
+
const objects = [base, current, incoming].filter(isJsonObject);
|
|
4146
|
+
const keys = [...new Set(objects.flatMap((value) => Object.keys(value)))].sort();
|
|
4147
|
+
return keys.flatMap((key) => buildThreeWayDiff(
|
|
4148
|
+
isJsonObject(base) ? base[key] : void 0,
|
|
4149
|
+
isJsonObject(current) ? current[key] : void 0,
|
|
4150
|
+
isJsonObject(incoming) ? incoming[key] : void 0,
|
|
4151
|
+
`${path}.${key}`
|
|
4152
|
+
));
|
|
4153
|
+
}
|
|
4154
|
+
return [{ path, kind: classify(base, current, incoming), base, current, incoming }];
|
|
4155
|
+
}
|
|
4156
|
+
function summarizeDiff(entries, resolutions = {}) {
|
|
4157
|
+
const summary = { added: 0, removed: 0, changed: 0, conflicted: 0, resolved: 0, pending: 0 };
|
|
4158
|
+
entries.forEach((entry) => {
|
|
4159
|
+
summary[entry.kind] += 1;
|
|
4160
|
+
if ((resolutions[entry.path] ?? "pending") === "pending") summary.pending += 1;
|
|
4161
|
+
else summary.resolved += 1;
|
|
4162
|
+
});
|
|
4163
|
+
return summary;
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
// src/components/DiffViewer.tsx
|
|
4167
|
+
var import_jsx_runtime56 = require("react/jsx-runtime");
|
|
4168
|
+
var GROUPS = [
|
|
4169
|
+
{ kind: "conflicted", label: "Conflicted" },
|
|
4170
|
+
{ kind: "changed", label: "Changed" },
|
|
4171
|
+
{ kind: "added", label: "Added" },
|
|
4172
|
+
{ kind: "removed", label: "Removed" }
|
|
4173
|
+
];
|
|
4174
|
+
var DEFAULT_LABELS = {};
|
|
4175
|
+
var renderValue = (value) => value === void 0 ? "Not present" : JSON.stringify(value, null, 2);
|
|
4176
|
+
function DiffViewer({
|
|
4177
|
+
base,
|
|
4178
|
+
current,
|
|
4179
|
+
incoming,
|
|
4180
|
+
entries: providedEntries,
|
|
4181
|
+
resolutions: controlledResolutions,
|
|
4182
|
+
onResolutionChange,
|
|
4183
|
+
labels = DEFAULT_LABELS,
|
|
4184
|
+
className
|
|
4185
|
+
}) {
|
|
4186
|
+
const id = (0, import_react31.useId)();
|
|
4187
|
+
const entries = (0, import_react31.useMemo)(() => providedEntries ?? buildThreeWayDiff(base, current, incoming), [providedEntries, base, current, incoming]);
|
|
4188
|
+
const [internalResolutions, setInternalResolutions] = (0, import_react31.useState)({});
|
|
4189
|
+
const resolutions = controlledResolutions ?? internalResolutions;
|
|
4190
|
+
const summary = summarizeDiff(entries, resolutions);
|
|
4191
|
+
const resolve2 = (path, resolution) => {
|
|
4192
|
+
if (controlledResolutions === void 0) setInternalResolutions((currentState) => ({ ...currentState, [path]: resolution }));
|
|
4193
|
+
onResolutionChange?.(path, resolution);
|
|
4194
|
+
};
|
|
4195
|
+
if (entries.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("section", { className: cx("uix-diff-viewer uix-diff-viewer--empty", className), "aria-label": "Configuration differences", children: [
|
|
4196
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("h3", { children: "No differences" }),
|
|
4197
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("p", { children: "Base, current, and incoming values are aligned." })
|
|
4198
|
+
] });
|
|
4199
|
+
return /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("section", { className: cx("uix-diff-viewer", className), "aria-label": "Configuration differences", children: [
|
|
4200
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "uix-diff-viewer__summary", "aria-label": "Difference summary", children: [
|
|
4201
|
+
GROUPS.map((group) => /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("span", { children: [
|
|
4202
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("b", { children: summary[group.kind] }),
|
|
4203
|
+
" ",
|
|
4204
|
+
group.label.toLowerCase()
|
|
4205
|
+
] }, group.kind)),
|
|
4206
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("span", { children: [
|
|
4207
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("b", { children: summary.resolved }),
|
|
4208
|
+
" resolved"
|
|
4209
|
+
] }),
|
|
4210
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("span", { children: [
|
|
4211
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("b", { children: summary.pending }),
|
|
4212
|
+
" pending"
|
|
4213
|
+
] })
|
|
4214
|
+
] }),
|
|
4215
|
+
GROUPS.map((group) => {
|
|
4216
|
+
const grouped = entries.filter((entry) => entry.kind === group.kind);
|
|
4217
|
+
if (!grouped.length) return null;
|
|
4218
|
+
return /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("section", { className: "uix-diff-viewer__group", "aria-labelledby": `${id}-${group.kind}`, children: [
|
|
4219
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("h3", { id: `${id}-${group.kind}`, children: [
|
|
4220
|
+
group.label,
|
|
4221
|
+
" ",
|
|
4222
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("span", { children: grouped.length })
|
|
4223
|
+
] }),
|
|
4224
|
+
grouped.map((entry) => {
|
|
4225
|
+
const resolution = resolutions[entry.path] ?? "pending";
|
|
4226
|
+
return /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("details", { className: "uix-diff-viewer__entry", "data-kind": entry.kind, open: entry.kind === "conflicted", children: [
|
|
4227
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("summary", { children: [
|
|
4228
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("code", { children: entry.path }),
|
|
4229
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)(StatusPill, { tone: resolution === "pending" ? "warning" : "success", children: resolution })
|
|
4230
|
+
] }),
|
|
4231
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("div", { className: "uix-diff-viewer__versions", children: ["base", "current", "incoming"].map((version) => /* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { role: "region", "aria-label": `${labels[version] ?? `${version[0].toUpperCase()}${version.slice(1)}`} value for ${entry.path}`, children: [
|
|
4232
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("strong", { children: labels[version] ?? `${version[0].toUpperCase()}${version.slice(1)}` }),
|
|
4233
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("pre", { children: renderValue(entry[version]) })
|
|
4234
|
+
] }, version)) }),
|
|
4235
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("div", { className: "uix-diff-viewer__actions", "aria-label": `Resolve ${entry.path}`, children: [
|
|
4236
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("button", { type: "button", className: "uix-btn uix-btn--primary uix-btn--sm", "aria-pressed": resolution === "accept", onClick: () => resolve2(entry.path, "accept"), children: "Accept incoming" }),
|
|
4237
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("button", { type: "button", className: "uix-btn uix-btn--secondary uix-btn--sm", "aria-pressed": resolution === "skip", onClick: () => resolve2(entry.path, "skip"), children: "Keep current" }),
|
|
4238
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsx)("button", { type: "button", className: "uix-btn uix-btn--ghost uix-btn--sm", "aria-pressed": resolution === "pending", onClick: () => resolve2(entry.path, "pending"), children: "Mark pending" })
|
|
4239
|
+
] })
|
|
4240
|
+
] }, entry.path);
|
|
4241
|
+
})
|
|
4242
|
+
] }, group.kind);
|
|
4243
|
+
}),
|
|
4244
|
+
/* @__PURE__ */ (0, import_jsx_runtime56.jsxs)("p", { className: "uix-visually-hidden", "aria-live": "polite", children: [
|
|
4245
|
+
summary.resolved,
|
|
4246
|
+
" of ",
|
|
4247
|
+
entries.length,
|
|
4248
|
+
" differences resolved."
|
|
4249
|
+
] })
|
|
4250
|
+
] });
|
|
4251
|
+
}
|
|
2552
4252
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2553
4253
|
0 && (module.exports = {
|
|
2554
4254
|
Alert,
|
|
2555
4255
|
AppShell,
|
|
2556
4256
|
Avatar,
|
|
2557
4257
|
AvatarGroup,
|
|
4258
|
+
BrandProfileEditor,
|
|
4259
|
+
BrandProfiles,
|
|
4260
|
+
BuilderCanvas,
|
|
2558
4261
|
BulkBar,
|
|
2559
4262
|
Button,
|
|
2560
4263
|
ButtonGroup,
|
|
@@ -2562,6 +4265,7 @@ function StatusRow({ indicator, name, meta, className, ...props }) {
|
|
|
2562
4265
|
CellStrong,
|
|
2563
4266
|
CellSub,
|
|
2564
4267
|
Checkbox,
|
|
4268
|
+
ColorPicker,
|
|
2565
4269
|
CommandGroup,
|
|
2566
4270
|
CommandItem,
|
|
2567
4271
|
CommandPalette,
|
|
@@ -2571,9 +4275,11 @@ function StatusRow({ indicator, name, meta, className, ...props }) {
|
|
|
2571
4275
|
ComposerBar,
|
|
2572
4276
|
ContentList,
|
|
2573
4277
|
ContentListItem,
|
|
4278
|
+
DateRangePicker,
|
|
2574
4279
|
DescriptionItem,
|
|
2575
4280
|
DescriptionList,
|
|
2576
4281
|
DetailLayout,
|
|
4282
|
+
DiffViewer,
|
|
2577
4283
|
Drawer,
|
|
2578
4284
|
EmptyState,
|
|
2579
4285
|
ErrorState,
|
|
@@ -2594,11 +4300,14 @@ function StatusRow({ indicator, name, meta, className, ...props }) {
|
|
|
2594
4300
|
KanbanCard,
|
|
2595
4301
|
KanbanColumn,
|
|
2596
4302
|
Label,
|
|
4303
|
+
LicensePositionBar,
|
|
2597
4304
|
List,
|
|
2598
4305
|
ListItem,
|
|
2599
4306
|
LoadingState,
|
|
2600
4307
|
Mark,
|
|
4308
|
+
MatchReview,
|
|
2601
4309
|
Meter,
|
|
4310
|
+
MetricInput,
|
|
2602
4311
|
Modal,
|
|
2603
4312
|
NavFavourites,
|
|
2604
4313
|
NavGroup,
|
|
@@ -2615,9 +4324,12 @@ function StatusRow({ indicator, name, meta, className, ...props }) {
|
|
|
2615
4324
|
Prose,
|
|
2616
4325
|
Radio,
|
|
2617
4326
|
RadioGroup,
|
|
4327
|
+
RelationshipGraph,
|
|
2618
4328
|
ResourceGrid,
|
|
2619
4329
|
RowAction,
|
|
2620
4330
|
RowActions,
|
|
4331
|
+
RuleBuilder,
|
|
4332
|
+
SchedulingCalendar,
|
|
2621
4333
|
SectionHead,
|
|
2622
4334
|
Segmented,
|
|
2623
4335
|
SegmentedOption,
|
|
@@ -2648,29 +4360,68 @@ function StatusRow({ indicator, name, meta, className, ...props }) {
|
|
|
2648
4360
|
Tr,
|
|
2649
4361
|
Tree,
|
|
2650
4362
|
UserChip,
|
|
4363
|
+
addCalendarDays,
|
|
4364
|
+
addCalendarMonths,
|
|
4365
|
+
appendRuleNode,
|
|
4366
|
+
applyBrandProfile,
|
|
2651
4367
|
applyFilters,
|
|
4368
|
+
boundRelationshipGraph,
|
|
4369
|
+
buildMonthGrid,
|
|
4370
|
+
buildThreeWayDiff,
|
|
4371
|
+
clampMetricValue,
|
|
2652
4372
|
clampWidth,
|
|
2653
4373
|
computePosition,
|
|
4374
|
+
contrastRatio,
|
|
2654
4375
|
cx,
|
|
4376
|
+
enumerateDateSpan,
|
|
4377
|
+
findRuleNodeDepth,
|
|
2655
4378
|
flattenTree,
|
|
4379
|
+
hexToRgb,
|
|
2656
4380
|
highlightSegments,
|
|
4381
|
+
hsvToHex,
|
|
4382
|
+
hsvToRgb,
|
|
4383
|
+
isDateInRange,
|
|
4384
|
+
isDateUnavailable,
|
|
4385
|
+
isRuleGroup,
|
|
4386
|
+
layoutRelationshipGraph,
|
|
4387
|
+
mapRuleGroup,
|
|
2657
4388
|
matchFilter,
|
|
4389
|
+
meetsContrast,
|
|
2658
4390
|
mergePinned,
|
|
4391
|
+
moveRuleNode,
|
|
2659
4392
|
multiSort,
|
|
2660
4393
|
nextSortDir,
|
|
4394
|
+
normalizeHex,
|
|
4395
|
+
parseMetricValue,
|
|
2661
4396
|
parseView,
|
|
4397
|
+
relationshipNeighbors,
|
|
4398
|
+
removeRuleNode,
|
|
2662
4399
|
reorder,
|
|
4400
|
+
restoreBrandProfile,
|
|
4401
|
+
rgbToHex,
|
|
4402
|
+
rgbToHsv,
|
|
4403
|
+
ruleDepth,
|
|
2663
4404
|
searchRows,
|
|
2664
4405
|
selectAllState,
|
|
4406
|
+
selectRangeDate,
|
|
2665
4407
|
serializeView,
|
|
2666
4408
|
shouldVirtualize,
|
|
4409
|
+
startOfMonth,
|
|
4410
|
+
stepMetricValue,
|
|
4411
|
+
summarizeDiff,
|
|
4412
|
+
summarizeRule,
|
|
4413
|
+
toDateKey,
|
|
2667
4414
|
toggleId,
|
|
2668
4415
|
togglePage,
|
|
2669
4416
|
toggleSort,
|
|
4417
|
+
traverseRelationshipNode,
|
|
2670
4418
|
treeNav,
|
|
2671
4419
|
useAnchoredPosition,
|
|
2672
4420
|
useDialog,
|
|
2673
4421
|
useTable,
|
|
2674
4422
|
useVirtualRows,
|
|
2675
|
-
|
|
4423
|
+
validateRuleDefinition,
|
|
4424
|
+
virtualWindow,
|
|
4425
|
+
zonedDateKey,
|
|
4426
|
+
zonedDateSpan
|
|
2676
4427
|
});
|