react-matchings 0.1.0 → 0.1.1

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 CHANGED
@@ -4,13 +4,15 @@ A React component for building question-and-answer matching interactions. It ren
4
4
 
5
5
  Default styles are included automatically when the component is imported. Consumers do not need to import a separate CSS file.
6
6
 
7
+ [View the package on npm](https://www.npmjs.com/package/react-matchings)
8
+
7
9
  ## Features
8
10
 
9
11
  - Drag-to-connect matching interaction
10
12
  - Pointer support for mouse, touch, and pen input
11
13
  - Automatic scrolling while dragging near a scroll container edge
12
14
  - One-to-one answers by default, with optional answer reuse
13
- - Controlled change callback for saving or validating answers
15
+ - Controlled matches and change callback for saving or validating answers
14
16
  - Custom classes for the container, question buttons, and answer buttons
15
17
  - Configurable connector line color, endpoint color, radius, and offset
16
18
  - Per-match styles for validation feedback
@@ -65,6 +67,8 @@ function App() {
65
67
  | ------------------- | -------------------------------- | ----------- | ------------------------------------------------------------ |
66
68
  | `questions` | `{ id: number; text: string }[]` | Required | Items rendered in the left column. |
67
69
  | `answers` | `{ id: number; text: string }[]` | Required | Items rendered in the right column. |
70
+ | `matches` | `TMatch[]` | `undefined` | Controlled match list. Use with `onChange` to own state. |
71
+ | `defaultMatches` | `TMatch[]` | `undefined` | Initial match list for uncontrolled usage. |
68
72
  | `onChange` | `(matches: TMatch[]) => void` | `undefined` | Called whenever the user creates or removes a match. |
69
73
  | `className` | `string` | `undefined` | Additional classes for the root container. |
70
74
  | `questionClassName` | `string` | `undefined` | Additional classes for question buttons. |
@@ -107,6 +111,32 @@ const handleChange = (matches: TMatch[]) => {
107
111
  };
108
112
  ```
109
113
 
114
+ Use `defaultMatches` when you only need to seed the initial state:
115
+
116
+ ```tsx
117
+ <Matching
118
+ questions={questions}
119
+ answers={answers}
120
+ defaultMatches={[{ questionId: 1, answerId: 2 }]}
121
+ onChange={setMatches}
122
+ />
123
+ ```
124
+
125
+ Use `matches` when the parent owns the current value:
126
+
127
+ ```tsx
128
+ const [matches, setMatches] = useState<TMatch[]>([
129
+ { questionId: 1, answerId: 2 },
130
+ ]);
131
+
132
+ <Matching
133
+ questions={questions}
134
+ answers={answers}
135
+ matches={matches}
136
+ onChange={setMatches}
137
+ />
138
+ ```
139
+
110
140
  ## Styling
111
141
 
112
142
  The component ships with default styles and injects them automatically in the browser. You can override the default button and layout styles with class props:
@@ -168,6 +198,7 @@ export function Assessment() {
168
198
  <Matching
169
199
  questions={questions}
170
200
  answers={answers}
201
+ matches={matches}
171
202
  onChange={setMatches}
172
203
  disabled={submitted}
173
204
  getMatchStyles={(match) =>
@@ -227,7 +258,7 @@ npm pack
227
258
  Then install the generated tarball in a test React application:
228
259
 
229
260
  ```bash
230
- npm install /absolute/path/to/react-matchings/react-matchings-0.0.8.tgz
261
+ npm install /absolute/path/to/react-matchings/react-matchings-0.1.0.tgz
231
262
  ```
232
263
 
233
264
  Import only the component:
package/dist/index.d.mts CHANGED
@@ -23,6 +23,8 @@ type MatchingProps = {
23
23
  id: number;
24
24
  text: string;
25
25
  }[];
26
+ matches?: TMatch[];
27
+ defaultMatches?: TMatch[];
26
28
  className?: string;
27
29
  questionClassName?: string;
28
30
  answerClassName?: string;
@@ -36,6 +38,6 @@ type MatchingProps = {
36
38
  getMatchStyles?: (match: TMatch) => TMatchStyles | undefined;
37
39
  onChange?: (matches: TMatch[]) => void;
38
40
  };
39
- declare function Matching({ questions, answers, className, questionClassName, answerClassName, lineColor, circleColor, circleRadius, offset, disabled, allowAnswerReuse, autoScroll, getMatchStyles, onChange, }: MatchingProps): react_jsx_runtime.JSX.Element;
41
+ declare function Matching({ questions, answers, matches: controlledMatches, defaultMatches, className, questionClassName, answerClassName, lineColor, circleColor, circleRadius, offset, disabled, allowAnswerReuse, autoScroll, getMatchStyles, onChange, }: MatchingProps): react_jsx_runtime.JSX.Element;
40
42
 
41
43
  export { Matching, type MatchingProps, type TAutoScrollOptions, type TMatch, type TMatchStyles };
package/dist/index.d.ts CHANGED
@@ -23,6 +23,8 @@ type MatchingProps = {
23
23
  id: number;
24
24
  text: string;
25
25
  }[];
26
+ matches?: TMatch[];
27
+ defaultMatches?: TMatch[];
26
28
  className?: string;
27
29
  questionClassName?: string;
28
30
  answerClassName?: string;
@@ -36,6 +38,6 @@ type MatchingProps = {
36
38
  getMatchStyles?: (match: TMatch) => TMatchStyles | undefined;
37
39
  onChange?: (matches: TMatch[]) => void;
38
40
  };
39
- declare function Matching({ questions, answers, className, questionClassName, answerClassName, lineColor, circleColor, circleRadius, offset, disabled, allowAnswerReuse, autoScroll, getMatchStyles, onChange, }: MatchingProps): react_jsx_runtime.JSX.Element;
41
+ declare function Matching({ questions, answers, matches: controlledMatches, defaultMatches, className, questionClassName, answerClassName, lineColor, circleColor, circleRadius, offset, disabled, allowAnswerReuse, autoScroll, getMatchStyles, onChange, }: MatchingProps): react_jsx_runtime.JSX.Element;
40
42
 
41
43
  export { Matching, type MatchingProps, type TAutoScrollOptions, type TMatch, type TMatchStyles };
package/dist/index.js CHANGED
@@ -53,6 +53,12 @@ function toMatches(matches) {
53
53
  answerId
54
54
  }));
55
55
  }
56
+ function toMatchRecord(matches) {
57
+ return (matches ?? []).reduce((record, match) => {
58
+ record[match.questionId] = match.answerId;
59
+ return record;
60
+ }, {});
61
+ }
56
62
  function findScrollableAncestor(element) {
57
63
  let current = element.parentElement;
58
64
  while (current) {
@@ -67,6 +73,8 @@ function findScrollableAncestor(element) {
67
73
  function Matching({
68
74
  questions,
69
75
  answers,
76
+ matches: controlledMatches,
77
+ defaultMatches,
70
78
  className,
71
79
  questionClassName,
72
80
  answerClassName,
@@ -80,7 +88,9 @@ function Matching({
80
88
  getMatchStyles,
81
89
  onChange
82
90
  }) {
83
- const [matches, setMatches] = (0, import_react.useState)({});
91
+ const [uncontrolledMatches, setUncontrolledMatches] = (0, import_react.useState)(
92
+ () => toMatchRecord(defaultMatches)
93
+ );
84
94
  const [lines, setLines] = (0, import_react.useState)([]);
85
95
  const [dragging, setDragging] = (0, import_react.useState)(null);
86
96
  const [dragLine, setDragLine] = (0, import_react.useState)(null);
@@ -90,6 +100,20 @@ function Matching({
90
100
  const pointerRef = (0, import_react.useRef)(null);
91
101
  const scrollElementRef = (0, import_react.useRef)(null);
92
102
  const scrollFrameRef = (0, import_react.useRef)(null);
103
+ const isControlled = controlledMatches !== void 0;
104
+ const matches = (0, import_react.useMemo)(
105
+ () => isControlled ? toMatchRecord(controlledMatches) : uncontrolledMatches,
106
+ [controlledMatches, isControlled, uncontrolledMatches]
107
+ );
108
+ const setMatches = (0, import_react.useCallback)(
109
+ (next) => {
110
+ if (!isControlled) {
111
+ setUncontrolledMatches(next);
112
+ }
113
+ queueMicrotask(() => onChange?.(toMatches(next)));
114
+ },
115
+ [isControlled, onChange]
116
+ );
93
117
  const getElementCenter = (0, import_react.useCallback)(
94
118
  (element, isAnswer = false) => {
95
119
  if (!element || !containerRef.current) return null;
@@ -177,27 +201,21 @@ function Matching({
177
201
  );
178
202
  const handlePointerUp = (event, answerId) => {
179
203
  if (dragging != null && pointerRef.current?.id === event.pointerId) {
180
- setMatches((current) => {
181
- const next = { ...current };
182
- if (!allowAnswerReuse) {
183
- for (const [questionId, matchedAnswerId] of Object.entries(next)) {
184
- if (matchedAnswerId === answerId) delete next[Number(questionId)];
185
- }
204
+ const next = { ...matches };
205
+ if (!allowAnswerReuse) {
206
+ for (const [questionId, matchedAnswerId] of Object.entries(next)) {
207
+ if (matchedAnswerId === answerId) delete next[Number(questionId)];
186
208
  }
187
- next[dragging] = answerId;
188
- queueMicrotask(() => onChange?.(toMatches(next)));
189
- return next;
190
- });
209
+ }
210
+ next[dragging] = answerId;
211
+ setMatches(next);
191
212
  }
192
213
  cancelDragging();
193
214
  };
194
215
  const removeMatch = (questionId) => {
195
- setMatches((current) => {
196
- const next = { ...current };
197
- delete next[questionId];
198
- queueMicrotask(() => onChange?.(toMatches(next)));
199
- return next;
200
- });
216
+ const next = { ...matches };
217
+ delete next[questionId];
218
+ setMatches(next);
201
219
  };
202
220
  (0, import_react.useEffect)(() => {
203
221
  if (dragging == null) return;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/matching.tsx","../src/lib/utils.ts"],"sourcesContent":["export { Matching } from \"./matching\";\nexport type {\n MatchingProps,\n TAutoScrollOptions,\n TMatch,\n TMatchStyles,\n} from \"./matching\";\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"./lib/utils\";\n\nexport type TMatch = {\n questionId: number;\n answerId: number;\n};\n\nexport type TMatchStyles = {\n lineColor?: string;\n circleColor?: string;\n questionClassName?: string;\n answerClassName?: string;\n};\n\nexport type TAutoScrollOptions = {\n edgeThreshold?: number;\n maxSpeed?: number;\n};\n\nexport type MatchingProps = {\n questions: { id: number; text: string }[];\n answers: { id: number; text: string }[];\n className?: string;\n questionClassName?: string;\n answerClassName?: string;\n lineColor?: string;\n circleColor?: string;\n circleRadius?: number;\n offset?: number;\n disabled?: boolean;\n allowAnswerReuse?: boolean;\n autoScroll?: boolean | TAutoScrollOptions;\n getMatchStyles?: (match: TMatch) => TMatchStyles | undefined;\n onChange?: (matches: TMatch[]) => void;\n};\n\ntype Point = { x: number; y: number };\n\ntype Line = TMatch & {\n start: Point;\n end: Point;\n};\n\nconst DEFAULT_EDGE_THRESHOLD = 64;\nconst DEFAULT_MAX_SCROLL_SPEED = 16;\n\nfunction toMatches(matches: Record<number, number>): TMatch[] {\n return Object.entries(matches).map(([questionId, answerId]) => ({\n questionId: Number(questionId),\n answerId,\n }));\n}\n\nfunction findScrollableAncestor(element: HTMLElement): HTMLElement {\n let current = element.parentElement;\n\n while (current) {\n const { overflowY } = window.getComputedStyle(current);\n if (/(auto|scroll|overlay)/.test(overflowY) && current.scrollHeight > current.clientHeight) {\n return current;\n }\n current = current.parentElement;\n }\n\n return (document.scrollingElement as HTMLElement | null) ?? document.documentElement;\n}\n\nexport function Matching({\n questions,\n answers,\n className,\n questionClassName,\n answerClassName,\n lineColor = \"black\",\n circleColor = \"white\",\n circleRadius = 8,\n offset = 10,\n disabled,\n allowAnswerReuse = false,\n autoScroll = true,\n getMatchStyles,\n onChange,\n}: MatchingProps) {\n const [matches, setMatches] = useState<Record<number, number>>({});\n const [lines, setLines] = useState<Line[]>([]);\n const [dragging, setDragging] = useState<number | null>(null);\n const [dragLine, setDragLine] = useState<{ start: Point; end: Point } | null>(null);\n\n const containerRef = useRef<HTMLDivElement | null>(null);\n const questionRefs = useRef<Record<number, HTMLButtonElement | null>>({});\n const answerRefs = useRef<Record<number, HTMLButtonElement | null>>({});\n const pointerRef = useRef<{ id: number; clientX: number; clientY: number } | null>(null);\n const scrollElementRef = useRef<HTMLElement | null>(null);\n const scrollFrameRef = useRef<number | null>(null);\n\n const getElementCenter = useCallback(\n (element: HTMLElement | null, isAnswer = false) => {\n if (!element || !containerRef.current) return null;\n const rect = element.getBoundingClientRect();\n const containerRect = containerRef.current.getBoundingClientRect();\n return {\n x: isAnswer\n ? rect.left - containerRect.left + circleRadius + offset\n : rect.right - containerRect.left - circleRadius - offset,\n y: rect.top + rect.height / 2 - containerRect.top,\n };\n },\n [circleRadius, offset]\n );\n\n const refreshLines = useCallback(() => {\n const nextLines = toMatches(matches)\n .map(({ questionId, answerId }) => {\n const start = getElementCenter(questionRefs.current[questionId]);\n const end = getElementCenter(answerRefs.current[answerId], true);\n return start && end ? { questionId, answerId, start, end } : null;\n })\n .filter((line): line is Line => line !== null);\n\n setLines(nextLines);\n }, [getElementCenter, matches]);\n\n const refreshDragLine = useCallback(() => {\n if (dragging == null || !containerRef.current || !pointerRef.current) return;\n const start = getElementCenter(questionRefs.current[dragging]);\n if (!start) return;\n const rect = containerRef.current.getBoundingClientRect();\n setDragLine({\n start,\n end: {\n x: pointerRef.current.clientX - rect.left,\n y: pointerRef.current.clientY - rect.top,\n },\n });\n }, [dragging, getElementCenter]);\n\n const stopAutoScroll = useCallback(() => {\n if (scrollFrameRef.current !== null) cancelAnimationFrame(scrollFrameRef.current);\n scrollFrameRef.current = null;\n scrollElementRef.current = null;\n }, []);\n\n const runAutoScroll = useCallback(() => {\n scrollFrameRef.current = null;\n if (autoScroll === false || dragging == null || !pointerRef.current) return;\n\n const scrollElement = scrollElementRef.current;\n if (!scrollElement) return;\n\n const isDocument = scrollElement === document.scrollingElement;\n const rect = isDocument\n ? { top: 0, bottom: window.innerHeight }\n : scrollElement.getBoundingClientRect();\n const edgeThreshold =\n typeof autoScroll === \"object\"\n ? (autoScroll.edgeThreshold ?? DEFAULT_EDGE_THRESHOLD)\n : DEFAULT_EDGE_THRESHOLD;\n const maxSpeed =\n typeof autoScroll === \"object\"\n ? (autoScroll.maxSpeed ?? DEFAULT_MAX_SCROLL_SPEED)\n : DEFAULT_MAX_SCROLL_SPEED;\n const { clientY } = pointerRef.current;\n let speed = 0;\n\n if (clientY < rect.top + edgeThreshold) {\n speed = -maxSpeed * (1 - Math.max(0, clientY - rect.top) / edgeThreshold);\n } else if (clientY > rect.bottom - edgeThreshold) {\n speed = maxSpeed * (1 - Math.max(0, rect.bottom - clientY) / edgeThreshold);\n }\n\n if (speed !== 0) {\n const previousScrollTop = scrollElement.scrollTop;\n scrollElement.scrollTop += speed;\n if (scrollElement.scrollTop !== previousScrollTop) {\n refreshLines();\n refreshDragLine();\n }\n }\n\n scrollFrameRef.current = requestAnimationFrame(runAutoScroll);\n }, [autoScroll, dragging, refreshDragLine, refreshLines]);\n\n const cancelDragging = useCallback(() => {\n setDragging(null);\n setDragLine(null);\n pointerRef.current = null;\n stopAutoScroll();\n }, [stopAutoScroll]);\n\n const handlePointerDown = (event: React.PointerEvent, questionId: number) => {\n if (disabled || !containerRef.current) return;\n event.preventDefault();\n pointerRef.current = { id: event.pointerId, clientX: event.clientX, clientY: event.clientY };\n scrollElementRef.current = findScrollableAncestor(containerRef.current);\n setDragging(questionId);\n };\n\n const handlePointerMove = useCallback(\n (event: PointerEvent) => {\n if (dragging == null || pointerRef.current?.id !== event.pointerId) return;\n pointerRef.current = { id: event.pointerId, clientX: event.clientX, clientY: event.clientY };\n refreshDragLine();\n },\n [dragging, refreshDragLine]\n );\n\n const handlePointerUp = (event: React.PointerEvent, answerId: number) => {\n if (dragging != null && pointerRef.current?.id === event.pointerId) {\n setMatches((current) => {\n const next = { ...current };\n if (!allowAnswerReuse) {\n for (const [questionId, matchedAnswerId] of Object.entries(next)) {\n if (matchedAnswerId === answerId) delete next[Number(questionId)];\n }\n }\n next[dragging] = answerId;\n queueMicrotask(() => onChange?.(toMatches(next)));\n return next;\n });\n }\n cancelDragging();\n };\n\n const removeMatch = (questionId: number) => {\n setMatches((current) => {\n const next = { ...current };\n delete next[questionId];\n queueMicrotask(() => onChange?.(toMatches(next)));\n return next;\n });\n };\n\n useEffect(() => {\n if (dragging == null) return;\n refreshDragLine();\n if (autoScroll !== false && scrollFrameRef.current === null) {\n scrollFrameRef.current = requestAnimationFrame(runAutoScroll);\n }\n\n const handleUp = (event: PointerEvent) => {\n if (pointerRef.current?.id === event.pointerId) cancelDragging();\n };\n document.addEventListener(\"pointermove\", handlePointerMove);\n document.addEventListener(\"pointerup\", handleUp);\n document.addEventListener(\"pointercancel\", handleUp);\n return () => {\n document.removeEventListener(\"pointermove\", handlePointerMove);\n document.removeEventListener(\"pointerup\", handleUp);\n document.removeEventListener(\"pointercancel\", handleUp);\n stopAutoScroll();\n };\n }, [\n autoScroll,\n cancelDragging,\n dragging,\n handlePointerMove,\n refreshDragLine,\n runAutoScroll,\n stopAutoScroll,\n ]);\n\n useEffect(() => {\n refreshLines();\n }, [refreshLines]);\n\n useEffect(() => {\n window.addEventListener(\"resize\", refreshLines);\n return () => window.removeEventListener(\"resize\", refreshLines);\n }, [refreshLines]);\n\n return (\n <div\n className={cn(\"grid relative grid-cols-2 gap-10 select-none\", className)}\n ref={containerRef}\n >\n <svg className=\"absolute z-20 w-full h-full pointer-events-none\">\n {lines.map((line) => {\n const styles = getMatchStyles?.(line);\n return (\n <g key={`${line.questionId}-${line.answerId}`}>\n <line\n x1={line.start.x}\n y1={line.start.y}\n x2={line.end.x}\n y2={line.end.y}\n stroke={styles?.lineColor ?? lineColor}\n strokeWidth={3}\n strokeLinecap=\"round\"\n />\n <circle\n cx={line.start.x}\n cy={line.start.y}\n r={circleRadius}\n fill={styles?.circleColor ?? circleColor}\n />\n <circle\n cx={line.end.x}\n cy={line.end.y}\n r={circleRadius}\n fill={styles?.circleColor ?? circleColor}\n />\n </g>\n );\n })}\n {dragLine && (\n <g>\n <line\n x1={dragLine.start.x}\n y1={dragLine.start.y}\n x2={dragLine.end.x}\n y2={dragLine.end.y}\n stroke={lineColor}\n strokeWidth={3}\n strokeDasharray=\"5,5\"\n strokeLinecap=\"round\"\n />\n <circle cx={dragLine.start.x} cy={dragLine.start.y} r={circleRadius} fill={circleColor} />\n <circle cx={dragLine.end.x} cy={dragLine.end.y} r={circleRadius} fill={circleColor} />\n </g>\n )}\n </svg>\n\n <div className=\"relative z-10 space-y-3\">\n {questions.map((question) => {\n const answerId = matches[question.id];\n const styles =\n answerId === undefined\n ? undefined\n : getMatchStyles?.({ questionId: question.id, answerId });\n return (\n <button\n key={question.id}\n ref={(element) => void (questionRefs.current[question.id] = element)}\n type=\"button\"\n disabled={disabled}\n onPointerDown={(event) => handlePointerDown(event, question.id)}\n onClick={() => answerId !== undefined && removeMatch(question.id)}\n className={cn(\n \"p-4 rounded bg-black text-white w-full touch-none font-medium focus:outline-none focus:ring-2 focus:ring-gray-500\",\n answerId !== undefined && \"bg-gray-700\",\n questionClassName,\n styles?.questionClassName\n )}\n >\n {question.text}\n </button>\n );\n })}\n </div>\n\n <div className=\"relative z-10 space-y-3\">\n {answers.map((answer) => {\n const answerMatches = toMatches(matches).filter((match) => match.answerId === answer.id);\n return (\n <button\n key={answer.id}\n ref={(element) => void (answerRefs.current[answer.id] = element)}\n type=\"button\"\n disabled={disabled}\n onPointerUp={(event) => handlePointerUp(event, answer.id)}\n className={cn(\n \"p-4 rounded bg-black text-white w-full touch-none font-medium focus:outline-none focus:ring-2 focus:ring-gray-500\",\n answerMatches.length > 0 && \"bg-gray-700\",\n answerClassName,\n answerMatches.map((match) => getMatchStyles?.(match)?.answerClassName)\n )}\n >\n {answer.text}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/matching.tsx","../src/lib/utils.ts"],"sourcesContent":["export { Matching } from \"./matching\";\nexport type {\n MatchingProps,\n TAutoScrollOptions,\n TMatch,\n TMatchStyles,\n} from \"./matching\";\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"./lib/utils\";\n\nexport type TMatch = {\n questionId: number;\n answerId: number;\n};\n\nexport type TMatchStyles = {\n lineColor?: string;\n circleColor?: string;\n questionClassName?: string;\n answerClassName?: string;\n};\n\nexport type TAutoScrollOptions = {\n edgeThreshold?: number;\n maxSpeed?: number;\n};\n\nexport type MatchingProps = {\n questions: { id: number; text: string }[];\n answers: { id: number; text: string }[];\n matches?: TMatch[];\n defaultMatches?: TMatch[];\n className?: string;\n questionClassName?: string;\n answerClassName?: string;\n lineColor?: string;\n circleColor?: string;\n circleRadius?: number;\n offset?: number;\n disabled?: boolean;\n allowAnswerReuse?: boolean;\n autoScroll?: boolean | TAutoScrollOptions;\n getMatchStyles?: (match: TMatch) => TMatchStyles | undefined;\n onChange?: (matches: TMatch[]) => void;\n};\n\ntype Point = { x: number; y: number };\n\ntype Line = TMatch & {\n start: Point;\n end: Point;\n};\n\nconst DEFAULT_EDGE_THRESHOLD = 64;\nconst DEFAULT_MAX_SCROLL_SPEED = 16;\n\nfunction toMatches(matches: Record<number, number>): TMatch[] {\n return Object.entries(matches).map(([questionId, answerId]) => ({\n questionId: Number(questionId),\n answerId,\n }));\n}\n\nfunction toMatchRecord(matches: TMatch[] | undefined): Record<number, number> {\n return (matches ?? []).reduce<Record<number, number>>((record, match) => {\n record[match.questionId] = match.answerId;\n return record;\n }, {});\n}\n\nfunction findScrollableAncestor(element: HTMLElement): HTMLElement {\n let current = element.parentElement;\n\n while (current) {\n const { overflowY } = window.getComputedStyle(current);\n if (/(auto|scroll|overlay)/.test(overflowY) && current.scrollHeight > current.clientHeight) {\n return current;\n }\n current = current.parentElement;\n }\n\n return (document.scrollingElement as HTMLElement | null) ?? document.documentElement;\n}\n\nexport function Matching({\n questions,\n answers,\n matches: controlledMatches,\n defaultMatches,\n className,\n questionClassName,\n answerClassName,\n lineColor = \"black\",\n circleColor = \"white\",\n circleRadius = 8,\n offset = 10,\n disabled,\n allowAnswerReuse = false,\n autoScroll = true,\n getMatchStyles,\n onChange,\n}: MatchingProps) {\n const [uncontrolledMatches, setUncontrolledMatches] = useState<Record<number, number>>(() =>\n toMatchRecord(defaultMatches)\n );\n const [lines, setLines] = useState<Line[]>([]);\n const [dragging, setDragging] = useState<number | null>(null);\n const [dragLine, setDragLine] = useState<{ start: Point; end: Point } | null>(null);\n\n const containerRef = useRef<HTMLDivElement | null>(null);\n const questionRefs = useRef<Record<number, HTMLButtonElement | null>>({});\n const answerRefs = useRef<Record<number, HTMLButtonElement | null>>({});\n const pointerRef = useRef<{ id: number; clientX: number; clientY: number } | null>(null);\n const scrollElementRef = useRef<HTMLElement | null>(null);\n const scrollFrameRef = useRef<number | null>(null);\n const isControlled = controlledMatches !== undefined;\n const matches = useMemo(\n () => (isControlled ? toMatchRecord(controlledMatches) : uncontrolledMatches),\n [controlledMatches, isControlled, uncontrolledMatches]\n );\n\n const setMatches = useCallback(\n (next: Record<number, number>) => {\n if (!isControlled) {\n setUncontrolledMatches(next);\n }\n queueMicrotask(() => onChange?.(toMatches(next)));\n },\n [isControlled, onChange]\n );\n\n const getElementCenter = useCallback(\n (element: HTMLElement | null, isAnswer = false) => {\n if (!element || !containerRef.current) return null;\n const rect = element.getBoundingClientRect();\n const containerRect = containerRef.current.getBoundingClientRect();\n return {\n x: isAnswer\n ? rect.left - containerRect.left + circleRadius + offset\n : rect.right - containerRect.left - circleRadius - offset,\n y: rect.top + rect.height / 2 - containerRect.top,\n };\n },\n [circleRadius, offset]\n );\n\n const refreshLines = useCallback(() => {\n const nextLines = toMatches(matches)\n .map(({ questionId, answerId }) => {\n const start = getElementCenter(questionRefs.current[questionId]);\n const end = getElementCenter(answerRefs.current[answerId], true);\n return start && end ? { questionId, answerId, start, end } : null;\n })\n .filter((line): line is Line => line !== null);\n\n setLines(nextLines);\n }, [getElementCenter, matches]);\n\n const refreshDragLine = useCallback(() => {\n if (dragging == null || !containerRef.current || !pointerRef.current) return;\n const start = getElementCenter(questionRefs.current[dragging]);\n if (!start) return;\n const rect = containerRef.current.getBoundingClientRect();\n setDragLine({\n start,\n end: {\n x: pointerRef.current.clientX - rect.left,\n y: pointerRef.current.clientY - rect.top,\n },\n });\n }, [dragging, getElementCenter]);\n\n const stopAutoScroll = useCallback(() => {\n if (scrollFrameRef.current !== null) cancelAnimationFrame(scrollFrameRef.current);\n scrollFrameRef.current = null;\n scrollElementRef.current = null;\n }, []);\n\n const runAutoScroll = useCallback(() => {\n scrollFrameRef.current = null;\n if (autoScroll === false || dragging == null || !pointerRef.current) return;\n\n const scrollElement = scrollElementRef.current;\n if (!scrollElement) return;\n\n const isDocument = scrollElement === document.scrollingElement;\n const rect = isDocument\n ? { top: 0, bottom: window.innerHeight }\n : scrollElement.getBoundingClientRect();\n const edgeThreshold =\n typeof autoScroll === \"object\"\n ? (autoScroll.edgeThreshold ?? DEFAULT_EDGE_THRESHOLD)\n : DEFAULT_EDGE_THRESHOLD;\n const maxSpeed =\n typeof autoScroll === \"object\"\n ? (autoScroll.maxSpeed ?? DEFAULT_MAX_SCROLL_SPEED)\n : DEFAULT_MAX_SCROLL_SPEED;\n const { clientY } = pointerRef.current;\n let speed = 0;\n\n if (clientY < rect.top + edgeThreshold) {\n speed = -maxSpeed * (1 - Math.max(0, clientY - rect.top) / edgeThreshold);\n } else if (clientY > rect.bottom - edgeThreshold) {\n speed = maxSpeed * (1 - Math.max(0, rect.bottom - clientY) / edgeThreshold);\n }\n\n if (speed !== 0) {\n const previousScrollTop = scrollElement.scrollTop;\n scrollElement.scrollTop += speed;\n if (scrollElement.scrollTop !== previousScrollTop) {\n refreshLines();\n refreshDragLine();\n }\n }\n\n scrollFrameRef.current = requestAnimationFrame(runAutoScroll);\n }, [autoScroll, dragging, refreshDragLine, refreshLines]);\n\n const cancelDragging = useCallback(() => {\n setDragging(null);\n setDragLine(null);\n pointerRef.current = null;\n stopAutoScroll();\n }, [stopAutoScroll]);\n\n const handlePointerDown = (event: React.PointerEvent, questionId: number) => {\n if (disabled || !containerRef.current) return;\n event.preventDefault();\n pointerRef.current = { id: event.pointerId, clientX: event.clientX, clientY: event.clientY };\n scrollElementRef.current = findScrollableAncestor(containerRef.current);\n setDragging(questionId);\n };\n\n const handlePointerMove = useCallback(\n (event: PointerEvent) => {\n if (dragging == null || pointerRef.current?.id !== event.pointerId) return;\n pointerRef.current = { id: event.pointerId, clientX: event.clientX, clientY: event.clientY };\n refreshDragLine();\n },\n [dragging, refreshDragLine]\n );\n\n const handlePointerUp = (event: React.PointerEvent, answerId: number) => {\n if (dragging != null && pointerRef.current?.id === event.pointerId) {\n const next = { ...matches };\n if (!allowAnswerReuse) {\n for (const [questionId, matchedAnswerId] of Object.entries(next)) {\n if (matchedAnswerId === answerId) delete next[Number(questionId)];\n }\n }\n next[dragging] = answerId;\n setMatches(next);\n }\n cancelDragging();\n };\n\n const removeMatch = (questionId: number) => {\n const next = { ...matches };\n delete next[questionId];\n setMatches(next);\n };\n\n useEffect(() => {\n if (dragging == null) return;\n refreshDragLine();\n if (autoScroll !== false && scrollFrameRef.current === null) {\n scrollFrameRef.current = requestAnimationFrame(runAutoScroll);\n }\n\n const handleUp = (event: PointerEvent) => {\n if (pointerRef.current?.id === event.pointerId) cancelDragging();\n };\n document.addEventListener(\"pointermove\", handlePointerMove);\n document.addEventListener(\"pointerup\", handleUp);\n document.addEventListener(\"pointercancel\", handleUp);\n return () => {\n document.removeEventListener(\"pointermove\", handlePointerMove);\n document.removeEventListener(\"pointerup\", handleUp);\n document.removeEventListener(\"pointercancel\", handleUp);\n stopAutoScroll();\n };\n }, [\n autoScroll,\n cancelDragging,\n dragging,\n handlePointerMove,\n refreshDragLine,\n runAutoScroll,\n stopAutoScroll,\n ]);\n\n useEffect(() => {\n refreshLines();\n }, [refreshLines]);\n\n useEffect(() => {\n window.addEventListener(\"resize\", refreshLines);\n return () => window.removeEventListener(\"resize\", refreshLines);\n }, [refreshLines]);\n\n return (\n <div\n className={cn(\"grid relative grid-cols-2 gap-10 select-none\", className)}\n ref={containerRef}\n >\n <svg className=\"absolute z-20 w-full h-full pointer-events-none\">\n {lines.map((line) => {\n const styles = getMatchStyles?.(line);\n return (\n <g key={`${line.questionId}-${line.answerId}`}>\n <line\n x1={line.start.x}\n y1={line.start.y}\n x2={line.end.x}\n y2={line.end.y}\n stroke={styles?.lineColor ?? lineColor}\n strokeWidth={3}\n strokeLinecap=\"round\"\n />\n <circle\n cx={line.start.x}\n cy={line.start.y}\n r={circleRadius}\n fill={styles?.circleColor ?? circleColor}\n />\n <circle\n cx={line.end.x}\n cy={line.end.y}\n r={circleRadius}\n fill={styles?.circleColor ?? circleColor}\n />\n </g>\n );\n })}\n {dragLine && (\n <g>\n <line\n x1={dragLine.start.x}\n y1={dragLine.start.y}\n x2={dragLine.end.x}\n y2={dragLine.end.y}\n stroke={lineColor}\n strokeWidth={3}\n strokeDasharray=\"5,5\"\n strokeLinecap=\"round\"\n />\n <circle cx={dragLine.start.x} cy={dragLine.start.y} r={circleRadius} fill={circleColor} />\n <circle cx={dragLine.end.x} cy={dragLine.end.y} r={circleRadius} fill={circleColor} />\n </g>\n )}\n </svg>\n\n <div className=\"relative z-10 space-y-3\">\n {questions.map((question) => {\n const answerId = matches[question.id];\n const styles =\n answerId === undefined\n ? undefined\n : getMatchStyles?.({ questionId: question.id, answerId });\n return (\n <button\n key={question.id}\n ref={(element) => void (questionRefs.current[question.id] = element)}\n type=\"button\"\n disabled={disabled}\n onPointerDown={(event) => handlePointerDown(event, question.id)}\n onClick={() => answerId !== undefined && removeMatch(question.id)}\n className={cn(\n \"p-4 rounded bg-black text-white w-full touch-none font-medium focus:outline-none focus:ring-2 focus:ring-gray-500\",\n answerId !== undefined && \"bg-gray-700\",\n questionClassName,\n styles?.questionClassName\n )}\n >\n {question.text}\n </button>\n );\n })}\n </div>\n\n <div className=\"relative z-10 space-y-3\">\n {answers.map((answer) => {\n const answerMatches = toMatches(matches).filter((match) => match.answerId === answer.id);\n return (\n <button\n key={answer.id}\n ref={(element) => void (answerRefs.current[answer.id] = element)}\n type=\"button\"\n disabled={disabled}\n onPointerUp={(event) => handlePointerUp(event, answer.id)}\n className={cn(\n \"p-4 rounded bg-black text-white w-full touch-none font-medium focus:outline-none focus:ring-2 focus:ring-gray-500\",\n answerMatches.length > 0 && \"bg-gray-700\",\n answerClassName,\n answerMatches.map((match) => getMatchStyles?.(match)?.answerClassName)\n )}\n >\n {answer.text}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ function __injectReactMatchingsCss() {
8
8
  }
9
9
  __injectReactMatchingsCss();
10
10
  // src/matching.tsx
11
- import { useCallback, useEffect, useRef, useState } from "react";
11
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
12
12
 
13
13
  // src/lib/utils.ts
14
14
  import { clsx } from "clsx";
@@ -27,6 +27,12 @@ function toMatches(matches) {
27
27
  answerId
28
28
  }));
29
29
  }
30
+ function toMatchRecord(matches) {
31
+ return (matches ?? []).reduce((record, match) => {
32
+ record[match.questionId] = match.answerId;
33
+ return record;
34
+ }, {});
35
+ }
30
36
  function findScrollableAncestor(element) {
31
37
  let current = element.parentElement;
32
38
  while (current) {
@@ -41,6 +47,8 @@ function findScrollableAncestor(element) {
41
47
  function Matching({
42
48
  questions,
43
49
  answers,
50
+ matches: controlledMatches,
51
+ defaultMatches,
44
52
  className,
45
53
  questionClassName,
46
54
  answerClassName,
@@ -54,7 +62,9 @@ function Matching({
54
62
  getMatchStyles,
55
63
  onChange
56
64
  }) {
57
- const [matches, setMatches] = useState({});
65
+ const [uncontrolledMatches, setUncontrolledMatches] = useState(
66
+ () => toMatchRecord(defaultMatches)
67
+ );
58
68
  const [lines, setLines] = useState([]);
59
69
  const [dragging, setDragging] = useState(null);
60
70
  const [dragLine, setDragLine] = useState(null);
@@ -64,6 +74,20 @@ function Matching({
64
74
  const pointerRef = useRef(null);
65
75
  const scrollElementRef = useRef(null);
66
76
  const scrollFrameRef = useRef(null);
77
+ const isControlled = controlledMatches !== void 0;
78
+ const matches = useMemo(
79
+ () => isControlled ? toMatchRecord(controlledMatches) : uncontrolledMatches,
80
+ [controlledMatches, isControlled, uncontrolledMatches]
81
+ );
82
+ const setMatches = useCallback(
83
+ (next) => {
84
+ if (!isControlled) {
85
+ setUncontrolledMatches(next);
86
+ }
87
+ queueMicrotask(() => onChange?.(toMatches(next)));
88
+ },
89
+ [isControlled, onChange]
90
+ );
67
91
  const getElementCenter = useCallback(
68
92
  (element, isAnswer = false) => {
69
93
  if (!element || !containerRef.current) return null;
@@ -151,27 +175,21 @@ function Matching({
151
175
  );
152
176
  const handlePointerUp = (event, answerId) => {
153
177
  if (dragging != null && pointerRef.current?.id === event.pointerId) {
154
- setMatches((current) => {
155
- const next = { ...current };
156
- if (!allowAnswerReuse) {
157
- for (const [questionId, matchedAnswerId] of Object.entries(next)) {
158
- if (matchedAnswerId === answerId) delete next[Number(questionId)];
159
- }
178
+ const next = { ...matches };
179
+ if (!allowAnswerReuse) {
180
+ for (const [questionId, matchedAnswerId] of Object.entries(next)) {
181
+ if (matchedAnswerId === answerId) delete next[Number(questionId)];
160
182
  }
161
- next[dragging] = answerId;
162
- queueMicrotask(() => onChange?.(toMatches(next)));
163
- return next;
164
- });
183
+ }
184
+ next[dragging] = answerId;
185
+ setMatches(next);
165
186
  }
166
187
  cancelDragging();
167
188
  };
168
189
  const removeMatch = (questionId) => {
169
- setMatches((current) => {
170
- const next = { ...current };
171
- delete next[questionId];
172
- queueMicrotask(() => onChange?.(toMatches(next)));
173
- return next;
174
- });
190
+ const next = { ...matches };
191
+ delete next[questionId];
192
+ setMatches(next);
175
193
  };
176
194
  useEffect(() => {
177
195
  if (dragging == null) return;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/matching.tsx","../src/lib/utils.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"./lib/utils\";\n\nexport type TMatch = {\n questionId: number;\n answerId: number;\n};\n\nexport type TMatchStyles = {\n lineColor?: string;\n circleColor?: string;\n questionClassName?: string;\n answerClassName?: string;\n};\n\nexport type TAutoScrollOptions = {\n edgeThreshold?: number;\n maxSpeed?: number;\n};\n\nexport type MatchingProps = {\n questions: { id: number; text: string }[];\n answers: { id: number; text: string }[];\n className?: string;\n questionClassName?: string;\n answerClassName?: string;\n lineColor?: string;\n circleColor?: string;\n circleRadius?: number;\n offset?: number;\n disabled?: boolean;\n allowAnswerReuse?: boolean;\n autoScroll?: boolean | TAutoScrollOptions;\n getMatchStyles?: (match: TMatch) => TMatchStyles | undefined;\n onChange?: (matches: TMatch[]) => void;\n};\n\ntype Point = { x: number; y: number };\n\ntype Line = TMatch & {\n start: Point;\n end: Point;\n};\n\nconst DEFAULT_EDGE_THRESHOLD = 64;\nconst DEFAULT_MAX_SCROLL_SPEED = 16;\n\nfunction toMatches(matches: Record<number, number>): TMatch[] {\n return Object.entries(matches).map(([questionId, answerId]) => ({\n questionId: Number(questionId),\n answerId,\n }));\n}\n\nfunction findScrollableAncestor(element: HTMLElement): HTMLElement {\n let current = element.parentElement;\n\n while (current) {\n const { overflowY } = window.getComputedStyle(current);\n if (/(auto|scroll|overlay)/.test(overflowY) && current.scrollHeight > current.clientHeight) {\n return current;\n }\n current = current.parentElement;\n }\n\n return (document.scrollingElement as HTMLElement | null) ?? document.documentElement;\n}\n\nexport function Matching({\n questions,\n answers,\n className,\n questionClassName,\n answerClassName,\n lineColor = \"black\",\n circleColor = \"white\",\n circleRadius = 8,\n offset = 10,\n disabled,\n allowAnswerReuse = false,\n autoScroll = true,\n getMatchStyles,\n onChange,\n}: MatchingProps) {\n const [matches, setMatches] = useState<Record<number, number>>({});\n const [lines, setLines] = useState<Line[]>([]);\n const [dragging, setDragging] = useState<number | null>(null);\n const [dragLine, setDragLine] = useState<{ start: Point; end: Point } | null>(null);\n\n const containerRef = useRef<HTMLDivElement | null>(null);\n const questionRefs = useRef<Record<number, HTMLButtonElement | null>>({});\n const answerRefs = useRef<Record<number, HTMLButtonElement | null>>({});\n const pointerRef = useRef<{ id: number; clientX: number; clientY: number } | null>(null);\n const scrollElementRef = useRef<HTMLElement | null>(null);\n const scrollFrameRef = useRef<number | null>(null);\n\n const getElementCenter = useCallback(\n (element: HTMLElement | null, isAnswer = false) => {\n if (!element || !containerRef.current) return null;\n const rect = element.getBoundingClientRect();\n const containerRect = containerRef.current.getBoundingClientRect();\n return {\n x: isAnswer\n ? rect.left - containerRect.left + circleRadius + offset\n : rect.right - containerRect.left - circleRadius - offset,\n y: rect.top + rect.height / 2 - containerRect.top,\n };\n },\n [circleRadius, offset]\n );\n\n const refreshLines = useCallback(() => {\n const nextLines = toMatches(matches)\n .map(({ questionId, answerId }) => {\n const start = getElementCenter(questionRefs.current[questionId]);\n const end = getElementCenter(answerRefs.current[answerId], true);\n return start && end ? { questionId, answerId, start, end } : null;\n })\n .filter((line): line is Line => line !== null);\n\n setLines(nextLines);\n }, [getElementCenter, matches]);\n\n const refreshDragLine = useCallback(() => {\n if (dragging == null || !containerRef.current || !pointerRef.current) return;\n const start = getElementCenter(questionRefs.current[dragging]);\n if (!start) return;\n const rect = containerRef.current.getBoundingClientRect();\n setDragLine({\n start,\n end: {\n x: pointerRef.current.clientX - rect.left,\n y: pointerRef.current.clientY - rect.top,\n },\n });\n }, [dragging, getElementCenter]);\n\n const stopAutoScroll = useCallback(() => {\n if (scrollFrameRef.current !== null) cancelAnimationFrame(scrollFrameRef.current);\n scrollFrameRef.current = null;\n scrollElementRef.current = null;\n }, []);\n\n const runAutoScroll = useCallback(() => {\n scrollFrameRef.current = null;\n if (autoScroll === false || dragging == null || !pointerRef.current) return;\n\n const scrollElement = scrollElementRef.current;\n if (!scrollElement) return;\n\n const isDocument = scrollElement === document.scrollingElement;\n const rect = isDocument\n ? { top: 0, bottom: window.innerHeight }\n : scrollElement.getBoundingClientRect();\n const edgeThreshold =\n typeof autoScroll === \"object\"\n ? (autoScroll.edgeThreshold ?? DEFAULT_EDGE_THRESHOLD)\n : DEFAULT_EDGE_THRESHOLD;\n const maxSpeed =\n typeof autoScroll === \"object\"\n ? (autoScroll.maxSpeed ?? DEFAULT_MAX_SCROLL_SPEED)\n : DEFAULT_MAX_SCROLL_SPEED;\n const { clientY } = pointerRef.current;\n let speed = 0;\n\n if (clientY < rect.top + edgeThreshold) {\n speed = -maxSpeed * (1 - Math.max(0, clientY - rect.top) / edgeThreshold);\n } else if (clientY > rect.bottom - edgeThreshold) {\n speed = maxSpeed * (1 - Math.max(0, rect.bottom - clientY) / edgeThreshold);\n }\n\n if (speed !== 0) {\n const previousScrollTop = scrollElement.scrollTop;\n scrollElement.scrollTop += speed;\n if (scrollElement.scrollTop !== previousScrollTop) {\n refreshLines();\n refreshDragLine();\n }\n }\n\n scrollFrameRef.current = requestAnimationFrame(runAutoScroll);\n }, [autoScroll, dragging, refreshDragLine, refreshLines]);\n\n const cancelDragging = useCallback(() => {\n setDragging(null);\n setDragLine(null);\n pointerRef.current = null;\n stopAutoScroll();\n }, [stopAutoScroll]);\n\n const handlePointerDown = (event: React.PointerEvent, questionId: number) => {\n if (disabled || !containerRef.current) return;\n event.preventDefault();\n pointerRef.current = { id: event.pointerId, clientX: event.clientX, clientY: event.clientY };\n scrollElementRef.current = findScrollableAncestor(containerRef.current);\n setDragging(questionId);\n };\n\n const handlePointerMove = useCallback(\n (event: PointerEvent) => {\n if (dragging == null || pointerRef.current?.id !== event.pointerId) return;\n pointerRef.current = { id: event.pointerId, clientX: event.clientX, clientY: event.clientY };\n refreshDragLine();\n },\n [dragging, refreshDragLine]\n );\n\n const handlePointerUp = (event: React.PointerEvent, answerId: number) => {\n if (dragging != null && pointerRef.current?.id === event.pointerId) {\n setMatches((current) => {\n const next = { ...current };\n if (!allowAnswerReuse) {\n for (const [questionId, matchedAnswerId] of Object.entries(next)) {\n if (matchedAnswerId === answerId) delete next[Number(questionId)];\n }\n }\n next[dragging] = answerId;\n queueMicrotask(() => onChange?.(toMatches(next)));\n return next;\n });\n }\n cancelDragging();\n };\n\n const removeMatch = (questionId: number) => {\n setMatches((current) => {\n const next = { ...current };\n delete next[questionId];\n queueMicrotask(() => onChange?.(toMatches(next)));\n return next;\n });\n };\n\n useEffect(() => {\n if (dragging == null) return;\n refreshDragLine();\n if (autoScroll !== false && scrollFrameRef.current === null) {\n scrollFrameRef.current = requestAnimationFrame(runAutoScroll);\n }\n\n const handleUp = (event: PointerEvent) => {\n if (pointerRef.current?.id === event.pointerId) cancelDragging();\n };\n document.addEventListener(\"pointermove\", handlePointerMove);\n document.addEventListener(\"pointerup\", handleUp);\n document.addEventListener(\"pointercancel\", handleUp);\n return () => {\n document.removeEventListener(\"pointermove\", handlePointerMove);\n document.removeEventListener(\"pointerup\", handleUp);\n document.removeEventListener(\"pointercancel\", handleUp);\n stopAutoScroll();\n };\n }, [\n autoScroll,\n cancelDragging,\n dragging,\n handlePointerMove,\n refreshDragLine,\n runAutoScroll,\n stopAutoScroll,\n ]);\n\n useEffect(() => {\n refreshLines();\n }, [refreshLines]);\n\n useEffect(() => {\n window.addEventListener(\"resize\", refreshLines);\n return () => window.removeEventListener(\"resize\", refreshLines);\n }, [refreshLines]);\n\n return (\n <div\n className={cn(\"grid relative grid-cols-2 gap-10 select-none\", className)}\n ref={containerRef}\n >\n <svg className=\"absolute z-20 w-full h-full pointer-events-none\">\n {lines.map((line) => {\n const styles = getMatchStyles?.(line);\n return (\n <g key={`${line.questionId}-${line.answerId}`}>\n <line\n x1={line.start.x}\n y1={line.start.y}\n x2={line.end.x}\n y2={line.end.y}\n stroke={styles?.lineColor ?? lineColor}\n strokeWidth={3}\n strokeLinecap=\"round\"\n />\n <circle\n cx={line.start.x}\n cy={line.start.y}\n r={circleRadius}\n fill={styles?.circleColor ?? circleColor}\n />\n <circle\n cx={line.end.x}\n cy={line.end.y}\n r={circleRadius}\n fill={styles?.circleColor ?? circleColor}\n />\n </g>\n );\n })}\n {dragLine && (\n <g>\n <line\n x1={dragLine.start.x}\n y1={dragLine.start.y}\n x2={dragLine.end.x}\n y2={dragLine.end.y}\n stroke={lineColor}\n strokeWidth={3}\n strokeDasharray=\"5,5\"\n strokeLinecap=\"round\"\n />\n <circle cx={dragLine.start.x} cy={dragLine.start.y} r={circleRadius} fill={circleColor} />\n <circle cx={dragLine.end.x} cy={dragLine.end.y} r={circleRadius} fill={circleColor} />\n </g>\n )}\n </svg>\n\n <div className=\"relative z-10 space-y-3\">\n {questions.map((question) => {\n const answerId = matches[question.id];\n const styles =\n answerId === undefined\n ? undefined\n : getMatchStyles?.({ questionId: question.id, answerId });\n return (\n <button\n key={question.id}\n ref={(element) => void (questionRefs.current[question.id] = element)}\n type=\"button\"\n disabled={disabled}\n onPointerDown={(event) => handlePointerDown(event, question.id)}\n onClick={() => answerId !== undefined && removeMatch(question.id)}\n className={cn(\n \"p-4 rounded bg-black text-white w-full touch-none font-medium focus:outline-none focus:ring-2 focus:ring-gray-500\",\n answerId !== undefined && \"bg-gray-700\",\n questionClassName,\n styles?.questionClassName\n )}\n >\n {question.text}\n </button>\n );\n })}\n </div>\n\n <div className=\"relative z-10 space-y-3\">\n {answers.map((answer) => {\n const answerMatches = toMatches(matches).filter((match) => match.answerId === answer.id);\n return (\n <button\n key={answer.id}\n ref={(element) => void (answerRefs.current[answer.id] = element)}\n type=\"button\"\n disabled={disabled}\n onPointerUp={(event) => handlePointerUp(event, answer.id)}\n className={cn(\n \"p-4 rounded bg-black text-white w-full touch-none font-medium focus:outline-none focus:ring-2 focus:ring-gray-500\",\n answerMatches.length > 0 && \"bg-gray-700\",\n answerClassName,\n answerMatches.map((match) => getMatchStyles?.(match)?.answerClassName)\n )}\n >\n {answer.text}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/matching.tsx","../src/lib/utils.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"./lib/utils\";\n\nexport type TMatch = {\n questionId: number;\n answerId: number;\n};\n\nexport type TMatchStyles = {\n lineColor?: string;\n circleColor?: string;\n questionClassName?: string;\n answerClassName?: string;\n};\n\nexport type TAutoScrollOptions = {\n edgeThreshold?: number;\n maxSpeed?: number;\n};\n\nexport type MatchingProps = {\n questions: { id: number; text: string }[];\n answers: { id: number; text: string }[];\n matches?: TMatch[];\n defaultMatches?: TMatch[];\n className?: string;\n questionClassName?: string;\n answerClassName?: string;\n lineColor?: string;\n circleColor?: string;\n circleRadius?: number;\n offset?: number;\n disabled?: boolean;\n allowAnswerReuse?: boolean;\n autoScroll?: boolean | TAutoScrollOptions;\n getMatchStyles?: (match: TMatch) => TMatchStyles | undefined;\n onChange?: (matches: TMatch[]) => void;\n};\n\ntype Point = { x: number; y: number };\n\ntype Line = TMatch & {\n start: Point;\n end: Point;\n};\n\nconst DEFAULT_EDGE_THRESHOLD = 64;\nconst DEFAULT_MAX_SCROLL_SPEED = 16;\n\nfunction toMatches(matches: Record<number, number>): TMatch[] {\n return Object.entries(matches).map(([questionId, answerId]) => ({\n questionId: Number(questionId),\n answerId,\n }));\n}\n\nfunction toMatchRecord(matches: TMatch[] | undefined): Record<number, number> {\n return (matches ?? []).reduce<Record<number, number>>((record, match) => {\n record[match.questionId] = match.answerId;\n return record;\n }, {});\n}\n\nfunction findScrollableAncestor(element: HTMLElement): HTMLElement {\n let current = element.parentElement;\n\n while (current) {\n const { overflowY } = window.getComputedStyle(current);\n if (/(auto|scroll|overlay)/.test(overflowY) && current.scrollHeight > current.clientHeight) {\n return current;\n }\n current = current.parentElement;\n }\n\n return (document.scrollingElement as HTMLElement | null) ?? document.documentElement;\n}\n\nexport function Matching({\n questions,\n answers,\n matches: controlledMatches,\n defaultMatches,\n className,\n questionClassName,\n answerClassName,\n lineColor = \"black\",\n circleColor = \"white\",\n circleRadius = 8,\n offset = 10,\n disabled,\n allowAnswerReuse = false,\n autoScroll = true,\n getMatchStyles,\n onChange,\n}: MatchingProps) {\n const [uncontrolledMatches, setUncontrolledMatches] = useState<Record<number, number>>(() =>\n toMatchRecord(defaultMatches)\n );\n const [lines, setLines] = useState<Line[]>([]);\n const [dragging, setDragging] = useState<number | null>(null);\n const [dragLine, setDragLine] = useState<{ start: Point; end: Point } | null>(null);\n\n const containerRef = useRef<HTMLDivElement | null>(null);\n const questionRefs = useRef<Record<number, HTMLButtonElement | null>>({});\n const answerRefs = useRef<Record<number, HTMLButtonElement | null>>({});\n const pointerRef = useRef<{ id: number; clientX: number; clientY: number } | null>(null);\n const scrollElementRef = useRef<HTMLElement | null>(null);\n const scrollFrameRef = useRef<number | null>(null);\n const isControlled = controlledMatches !== undefined;\n const matches = useMemo(\n () => (isControlled ? toMatchRecord(controlledMatches) : uncontrolledMatches),\n [controlledMatches, isControlled, uncontrolledMatches]\n );\n\n const setMatches = useCallback(\n (next: Record<number, number>) => {\n if (!isControlled) {\n setUncontrolledMatches(next);\n }\n queueMicrotask(() => onChange?.(toMatches(next)));\n },\n [isControlled, onChange]\n );\n\n const getElementCenter = useCallback(\n (element: HTMLElement | null, isAnswer = false) => {\n if (!element || !containerRef.current) return null;\n const rect = element.getBoundingClientRect();\n const containerRect = containerRef.current.getBoundingClientRect();\n return {\n x: isAnswer\n ? rect.left - containerRect.left + circleRadius + offset\n : rect.right - containerRect.left - circleRadius - offset,\n y: rect.top + rect.height / 2 - containerRect.top,\n };\n },\n [circleRadius, offset]\n );\n\n const refreshLines = useCallback(() => {\n const nextLines = toMatches(matches)\n .map(({ questionId, answerId }) => {\n const start = getElementCenter(questionRefs.current[questionId]);\n const end = getElementCenter(answerRefs.current[answerId], true);\n return start && end ? { questionId, answerId, start, end } : null;\n })\n .filter((line): line is Line => line !== null);\n\n setLines(nextLines);\n }, [getElementCenter, matches]);\n\n const refreshDragLine = useCallback(() => {\n if (dragging == null || !containerRef.current || !pointerRef.current) return;\n const start = getElementCenter(questionRefs.current[dragging]);\n if (!start) return;\n const rect = containerRef.current.getBoundingClientRect();\n setDragLine({\n start,\n end: {\n x: pointerRef.current.clientX - rect.left,\n y: pointerRef.current.clientY - rect.top,\n },\n });\n }, [dragging, getElementCenter]);\n\n const stopAutoScroll = useCallback(() => {\n if (scrollFrameRef.current !== null) cancelAnimationFrame(scrollFrameRef.current);\n scrollFrameRef.current = null;\n scrollElementRef.current = null;\n }, []);\n\n const runAutoScroll = useCallback(() => {\n scrollFrameRef.current = null;\n if (autoScroll === false || dragging == null || !pointerRef.current) return;\n\n const scrollElement = scrollElementRef.current;\n if (!scrollElement) return;\n\n const isDocument = scrollElement === document.scrollingElement;\n const rect = isDocument\n ? { top: 0, bottom: window.innerHeight }\n : scrollElement.getBoundingClientRect();\n const edgeThreshold =\n typeof autoScroll === \"object\"\n ? (autoScroll.edgeThreshold ?? DEFAULT_EDGE_THRESHOLD)\n : DEFAULT_EDGE_THRESHOLD;\n const maxSpeed =\n typeof autoScroll === \"object\"\n ? (autoScroll.maxSpeed ?? DEFAULT_MAX_SCROLL_SPEED)\n : DEFAULT_MAX_SCROLL_SPEED;\n const { clientY } = pointerRef.current;\n let speed = 0;\n\n if (clientY < rect.top + edgeThreshold) {\n speed = -maxSpeed * (1 - Math.max(0, clientY - rect.top) / edgeThreshold);\n } else if (clientY > rect.bottom - edgeThreshold) {\n speed = maxSpeed * (1 - Math.max(0, rect.bottom - clientY) / edgeThreshold);\n }\n\n if (speed !== 0) {\n const previousScrollTop = scrollElement.scrollTop;\n scrollElement.scrollTop += speed;\n if (scrollElement.scrollTop !== previousScrollTop) {\n refreshLines();\n refreshDragLine();\n }\n }\n\n scrollFrameRef.current = requestAnimationFrame(runAutoScroll);\n }, [autoScroll, dragging, refreshDragLine, refreshLines]);\n\n const cancelDragging = useCallback(() => {\n setDragging(null);\n setDragLine(null);\n pointerRef.current = null;\n stopAutoScroll();\n }, [stopAutoScroll]);\n\n const handlePointerDown = (event: React.PointerEvent, questionId: number) => {\n if (disabled || !containerRef.current) return;\n event.preventDefault();\n pointerRef.current = { id: event.pointerId, clientX: event.clientX, clientY: event.clientY };\n scrollElementRef.current = findScrollableAncestor(containerRef.current);\n setDragging(questionId);\n };\n\n const handlePointerMove = useCallback(\n (event: PointerEvent) => {\n if (dragging == null || pointerRef.current?.id !== event.pointerId) return;\n pointerRef.current = { id: event.pointerId, clientX: event.clientX, clientY: event.clientY };\n refreshDragLine();\n },\n [dragging, refreshDragLine]\n );\n\n const handlePointerUp = (event: React.PointerEvent, answerId: number) => {\n if (dragging != null && pointerRef.current?.id === event.pointerId) {\n const next = { ...matches };\n if (!allowAnswerReuse) {\n for (const [questionId, matchedAnswerId] of Object.entries(next)) {\n if (matchedAnswerId === answerId) delete next[Number(questionId)];\n }\n }\n next[dragging] = answerId;\n setMatches(next);\n }\n cancelDragging();\n };\n\n const removeMatch = (questionId: number) => {\n const next = { ...matches };\n delete next[questionId];\n setMatches(next);\n };\n\n useEffect(() => {\n if (dragging == null) return;\n refreshDragLine();\n if (autoScroll !== false && scrollFrameRef.current === null) {\n scrollFrameRef.current = requestAnimationFrame(runAutoScroll);\n }\n\n const handleUp = (event: PointerEvent) => {\n if (pointerRef.current?.id === event.pointerId) cancelDragging();\n };\n document.addEventListener(\"pointermove\", handlePointerMove);\n document.addEventListener(\"pointerup\", handleUp);\n document.addEventListener(\"pointercancel\", handleUp);\n return () => {\n document.removeEventListener(\"pointermove\", handlePointerMove);\n document.removeEventListener(\"pointerup\", handleUp);\n document.removeEventListener(\"pointercancel\", handleUp);\n stopAutoScroll();\n };\n }, [\n autoScroll,\n cancelDragging,\n dragging,\n handlePointerMove,\n refreshDragLine,\n runAutoScroll,\n stopAutoScroll,\n ]);\n\n useEffect(() => {\n refreshLines();\n }, [refreshLines]);\n\n useEffect(() => {\n window.addEventListener(\"resize\", refreshLines);\n return () => window.removeEventListener(\"resize\", refreshLines);\n }, [refreshLines]);\n\n return (\n <div\n className={cn(\"grid relative grid-cols-2 gap-10 select-none\", className)}\n ref={containerRef}\n >\n <svg className=\"absolute z-20 w-full h-full pointer-events-none\">\n {lines.map((line) => {\n const styles = getMatchStyles?.(line);\n return (\n <g key={`${line.questionId}-${line.answerId}`}>\n <line\n x1={line.start.x}\n y1={line.start.y}\n x2={line.end.x}\n y2={line.end.y}\n stroke={styles?.lineColor ?? lineColor}\n strokeWidth={3}\n strokeLinecap=\"round\"\n />\n <circle\n cx={line.start.x}\n cy={line.start.y}\n r={circleRadius}\n fill={styles?.circleColor ?? circleColor}\n />\n <circle\n cx={line.end.x}\n cy={line.end.y}\n r={circleRadius}\n fill={styles?.circleColor ?? circleColor}\n />\n </g>\n );\n })}\n {dragLine && (\n <g>\n <line\n x1={dragLine.start.x}\n y1={dragLine.start.y}\n x2={dragLine.end.x}\n y2={dragLine.end.y}\n stroke={lineColor}\n strokeWidth={3}\n strokeDasharray=\"5,5\"\n strokeLinecap=\"round\"\n />\n <circle cx={dragLine.start.x} cy={dragLine.start.y} r={circleRadius} fill={circleColor} />\n <circle cx={dragLine.end.x} cy={dragLine.end.y} r={circleRadius} fill={circleColor} />\n </g>\n )}\n </svg>\n\n <div className=\"relative z-10 space-y-3\">\n {questions.map((question) => {\n const answerId = matches[question.id];\n const styles =\n answerId === undefined\n ? undefined\n : getMatchStyles?.({ questionId: question.id, answerId });\n return (\n <button\n key={question.id}\n ref={(element) => void (questionRefs.current[question.id] = element)}\n type=\"button\"\n disabled={disabled}\n onPointerDown={(event) => handlePointerDown(event, question.id)}\n onClick={() => answerId !== undefined && removeMatch(question.id)}\n className={cn(\n \"p-4 rounded bg-black text-white w-full touch-none font-medium focus:outline-none focus:ring-2 focus:ring-gray-500\",\n answerId !== undefined && \"bg-gray-700\",\n questionClassName,\n styles?.questionClassName\n )}\n >\n {question.text}\n </button>\n );\n })}\n </div>\n\n <div className=\"relative z-10 space-y-3\">\n {answers.map((answer) => {\n const answerMatches = toMatches(matches).filter((match) => match.answerId === answer.id);\n return (\n <button\n key={answer.id}\n ref={(element) => void (answerRefs.current[answer.id] = element)}\n type=\"button\"\n disabled={disabled}\n onPointerUp={(event) => handlePointerUp(event, answer.id)}\n className={cn(\n \"p-4 rounded bg-black text-white w-full touch-none font-medium focus:outline-none focus:ring-2 focus:ring-gray-500\",\n answerMatches.length > 0 && \"bg-gray-700\",\n answerClassName,\n answerMatches.map((match) => getMatchStyles?.(match)?.answerClassName)\n )}\n >\n {answer.text}\n </button>\n );\n })}\n </div>\n </div>\n );\n}\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,SAAS,QAAQ,gBAAgB;;;ACAlE,SAAS,YAA6B;AACtC,SAAS,eAAe;AAEjB,SAAS,MAAM,QAAsB;AAC1C,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;;;ADySY,SACE,KADF;AAhQZ,IAAM,yBAAyB;AAC/B,IAAM,2BAA2B;AAEjC,SAAS,UAAU,SAA2C;AAC5D,SAAO,OAAO,QAAQ,OAAO,EAAE,IAAI,CAAC,CAAC,YAAY,QAAQ,OAAO;AAAA,IAC9D,YAAY,OAAO,UAAU;AAAA,IAC7B;AAAA,EACF,EAAE;AACJ;AAEA,SAAS,cAAc,SAAuD;AAC5E,UAAQ,WAAW,CAAC,GAAG,OAA+B,CAAC,QAAQ,UAAU;AACvE,WAAO,MAAM,UAAU,IAAI,MAAM;AACjC,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AACP;AAEA,SAAS,uBAAuB,SAAmC;AACjE,MAAI,UAAU,QAAQ;AAEtB,SAAO,SAAS;AACd,UAAM,EAAE,UAAU,IAAI,OAAO,iBAAiB,OAAO;AACrD,QAAI,wBAAwB,KAAK,SAAS,KAAK,QAAQ,eAAe,QAAQ,cAAc;AAC1F,aAAO;AAAA,IACT;AACA,cAAU,QAAQ;AAAA,EACpB;AAEA,SAAQ,SAAS,oBAA2C,SAAS;AACvE;AAEO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,eAAe;AAAA,EACf,SAAS;AAAA,EACT;AAAA,EACA,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb;AAAA,EACA;AACF,GAAkB;AAChB,QAAM,CAAC,qBAAqB,sBAAsB,IAAI;AAAA,IAAiC,MACrF,cAAc,cAAc;AAAA,EAC9B;AACA,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAiB,CAAC,CAAC;AAC7C,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAC5D,QAAM,CAAC,UAAU,WAAW,IAAI,SAA8C,IAAI;AAElF,QAAM,eAAe,OAA8B,IAAI;AACvD,QAAM,eAAe,OAAiD,CAAC,CAAC;AACxE,QAAM,aAAa,OAAiD,CAAC,CAAC;AACtE,QAAM,aAAa,OAAgE,IAAI;AACvF,QAAM,mBAAmB,OAA2B,IAAI;AACxD,QAAM,iBAAiB,OAAsB,IAAI;AACjD,QAAM,eAAe,sBAAsB;AAC3C,QAAM,UAAU;AAAA,IACd,MAAO,eAAe,cAAc,iBAAiB,IAAI;AAAA,IACzD,CAAC,mBAAmB,cAAc,mBAAmB;AAAA,EACvD;AAEA,QAAM,aAAa;AAAA,IACjB,CAAC,SAAiC;AAChC,UAAI,CAAC,cAAc;AACjB,+BAAuB,IAAI;AAAA,MAC7B;AACA,qBAAe,MAAM,WAAW,UAAU,IAAI,CAAC,CAAC;AAAA,IAClD;AAAA,IACA,CAAC,cAAc,QAAQ;AAAA,EACzB;AAEA,QAAM,mBAAmB;AAAA,IACvB,CAAC,SAA6B,WAAW,UAAU;AACjD,UAAI,CAAC,WAAW,CAAC,aAAa,QAAS,QAAO;AAC9C,YAAM,OAAO,QAAQ,sBAAsB;AAC3C,YAAM,gBAAgB,aAAa,QAAQ,sBAAsB;AACjE,aAAO;AAAA,QACL,GAAG,WACC,KAAK,OAAO,cAAc,OAAO,eAAe,SAChD,KAAK,QAAQ,cAAc,OAAO,eAAe;AAAA,QACrD,GAAG,KAAK,MAAM,KAAK,SAAS,IAAI,cAAc;AAAA,MAChD;AAAA,IACF;AAAA,IACA,CAAC,cAAc,MAAM;AAAA,EACvB;AAEA,QAAM,eAAe,YAAY,MAAM;AACrC,UAAM,YAAY,UAAU,OAAO,EAChC,IAAI,CAAC,EAAE,YAAY,SAAS,MAAM;AACjC,YAAM,QAAQ,iBAAiB,aAAa,QAAQ,UAAU,CAAC;AAC/D,YAAM,MAAM,iBAAiB,WAAW,QAAQ,QAAQ,GAAG,IAAI;AAC/D,aAAO,SAAS,MAAM,EAAE,YAAY,UAAU,OAAO,IAAI,IAAI;AAAA,IAC/D,CAAC,EACA,OAAO,CAAC,SAAuB,SAAS,IAAI;AAE/C,aAAS,SAAS;AAAA,EACpB,GAAG,CAAC,kBAAkB,OAAO,CAAC;AAE9B,QAAM,kBAAkB,YAAY,MAAM;AACxC,QAAI,YAAY,QAAQ,CAAC,aAAa,WAAW,CAAC,WAAW,QAAS;AACtE,UAAM,QAAQ,iBAAiB,aAAa,QAAQ,QAAQ,CAAC;AAC7D,QAAI,CAAC,MAAO;AACZ,UAAM,OAAO,aAAa,QAAQ,sBAAsB;AACxD,gBAAY;AAAA,MACV;AAAA,MACA,KAAK;AAAA,QACH,GAAG,WAAW,QAAQ,UAAU,KAAK;AAAA,QACrC,GAAG,WAAW,QAAQ,UAAU,KAAK;AAAA,MACvC;AAAA,IACF,CAAC;AAAA,EACH,GAAG,CAAC,UAAU,gBAAgB,CAAC;AAE/B,QAAM,iBAAiB,YAAY,MAAM;AACvC,QAAI,eAAe,YAAY,KAAM,sBAAqB,eAAe,OAAO;AAChF,mBAAe,UAAU;AACzB,qBAAiB,UAAU;AAAA,EAC7B,GAAG,CAAC,CAAC;AAEL,QAAM,gBAAgB,YAAY,MAAM;AACtC,mBAAe,UAAU;AACzB,QAAI,eAAe,SAAS,YAAY,QAAQ,CAAC,WAAW,QAAS;AAErE,UAAM,gBAAgB,iBAAiB;AACvC,QAAI,CAAC,cAAe;AAEpB,UAAM,aAAa,kBAAkB,SAAS;AAC9C,UAAM,OAAO,aACT,EAAE,KAAK,GAAG,QAAQ,OAAO,YAAY,IACrC,cAAc,sBAAsB;AACxC,UAAM,gBACJ,OAAO,eAAe,WACjB,WAAW,iBAAiB,yBAC7B;AACN,UAAM,WACJ,OAAO,eAAe,WACjB,WAAW,YAAY,2BACxB;AACN,UAAM,EAAE,QAAQ,IAAI,WAAW;AAC/B,QAAI,QAAQ;AAEZ,QAAI,UAAU,KAAK,MAAM,eAAe;AACtC,cAAQ,CAAC,YAAY,IAAI,KAAK,IAAI,GAAG,UAAU,KAAK,GAAG,IAAI;AAAA,IAC7D,WAAW,UAAU,KAAK,SAAS,eAAe;AAChD,cAAQ,YAAY,IAAI,KAAK,IAAI,GAAG,KAAK,SAAS,OAAO,IAAI;AAAA,IAC/D;AAEA,QAAI,UAAU,GAAG;AACf,YAAM,oBAAoB,cAAc;AACxC,oBAAc,aAAa;AAC3B,UAAI,cAAc,cAAc,mBAAmB;AACjD,qBAAa;AACb,wBAAgB;AAAA,MAClB;AAAA,IACF;AAEA,mBAAe,UAAU,sBAAsB,aAAa;AAAA,EAC9D,GAAG,CAAC,YAAY,UAAU,iBAAiB,YAAY,CAAC;AAExD,QAAM,iBAAiB,YAAY,MAAM;AACvC,gBAAY,IAAI;AAChB,gBAAY,IAAI;AAChB,eAAW,UAAU;AACrB,mBAAe;AAAA,EACjB,GAAG,CAAC,cAAc,CAAC;AAEnB,QAAM,oBAAoB,CAAC,OAA2B,eAAuB;AAC3E,QAAI,YAAY,CAAC,aAAa,QAAS;AACvC,UAAM,eAAe;AACrB,eAAW,UAAU,EAAE,IAAI,MAAM,WAAW,SAAS,MAAM,SAAS,SAAS,MAAM,QAAQ;AAC3F,qBAAiB,UAAU,uBAAuB,aAAa,OAAO;AACtE,gBAAY,UAAU;AAAA,EACxB;AAEA,QAAM,oBAAoB;AAAA,IACxB,CAAC,UAAwB;AACvB,UAAI,YAAY,QAAQ,WAAW,SAAS,OAAO,MAAM,UAAW;AACpE,iBAAW,UAAU,EAAE,IAAI,MAAM,WAAW,SAAS,MAAM,SAAS,SAAS,MAAM,QAAQ;AAC3F,sBAAgB;AAAA,IAClB;AAAA,IACA,CAAC,UAAU,eAAe;AAAA,EAC5B;AAEA,QAAM,kBAAkB,CAAC,OAA2B,aAAqB;AACvE,QAAI,YAAY,QAAQ,WAAW,SAAS,OAAO,MAAM,WAAW;AAClE,YAAM,OAAO,EAAE,GAAG,QAAQ;AAC1B,UAAI,CAAC,kBAAkB;AACrB,mBAAW,CAAC,YAAY,eAAe,KAAK,OAAO,QAAQ,IAAI,GAAG;AAChE,cAAI,oBAAoB,SAAU,QAAO,KAAK,OAAO,UAAU,CAAC;AAAA,QAClE;AAAA,MACF;AACA,WAAK,QAAQ,IAAI;AACjB,iBAAW,IAAI;AAAA,IACjB;AACA,mBAAe;AAAA,EACjB;AAEA,QAAM,cAAc,CAAC,eAAuB;AAC1C,UAAM,OAAO,EAAE,GAAG,QAAQ;AAC1B,WAAO,KAAK,UAAU;AACtB,eAAW,IAAI;AAAA,EACjB;AAEA,YAAU,MAAM;AACd,QAAI,YAAY,KAAM;AACtB,oBAAgB;AAChB,QAAI,eAAe,SAAS,eAAe,YAAY,MAAM;AAC3D,qBAAe,UAAU,sBAAsB,aAAa;AAAA,IAC9D;AAEA,UAAM,WAAW,CAAC,UAAwB;AACxC,UAAI,WAAW,SAAS,OAAO,MAAM,UAAW,gBAAe;AAAA,IACjE;AACA,aAAS,iBAAiB,eAAe,iBAAiB;AAC1D,aAAS,iBAAiB,aAAa,QAAQ;AAC/C,aAAS,iBAAiB,iBAAiB,QAAQ;AACnD,WAAO,MAAM;AACX,eAAS,oBAAoB,eAAe,iBAAiB;AAC7D,eAAS,oBAAoB,aAAa,QAAQ;AAClD,eAAS,oBAAoB,iBAAiB,QAAQ;AACtD,qBAAe;AAAA,IACjB;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,YAAU,MAAM;AACd,iBAAa;AAAA,EACf,GAAG,CAAC,YAAY,CAAC;AAEjB,YAAU,MAAM;AACd,WAAO,iBAAiB,UAAU,YAAY;AAC9C,WAAO,MAAM,OAAO,oBAAoB,UAAU,YAAY;AAAA,EAChE,GAAG,CAAC,YAAY,CAAC;AAEjB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,gDAAgD,SAAS;AAAA,MACvE,KAAK;AAAA,MAEL;AAAA,6BAAC,SAAI,WAAU,mDACZ;AAAA,gBAAM,IAAI,CAAC,SAAS;AACnB,kBAAM,SAAS,iBAAiB,IAAI;AACpC,mBACE,qBAAC,OACC;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,IAAI,KAAK,MAAM;AAAA,kBACf,IAAI,KAAK,MAAM;AAAA,kBACf,IAAI,KAAK,IAAI;AAAA,kBACb,IAAI,KAAK,IAAI;AAAA,kBACb,QAAQ,QAAQ,aAAa;AAAA,kBAC7B,aAAa;AAAA,kBACb,eAAc;AAAA;AAAA,cAChB;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,IAAI,KAAK,MAAM;AAAA,kBACf,IAAI,KAAK,MAAM;AAAA,kBACf,GAAG;AAAA,kBACH,MAAM,QAAQ,eAAe;AAAA;AAAA,cAC/B;AAAA,cACA;AAAA,gBAAC;AAAA;AAAA,kBACC,IAAI,KAAK,IAAI;AAAA,kBACb,IAAI,KAAK,IAAI;AAAA,kBACb,GAAG;AAAA,kBACH,MAAM,QAAQ,eAAe;AAAA;AAAA,cAC/B;AAAA,iBArBM,GAAG,KAAK,UAAU,IAAI,KAAK,QAAQ,EAsB3C;AAAA,UAEJ,CAAC;AAAA,UACA,YACC,qBAAC,OACC;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,IAAI,SAAS,MAAM;AAAA,gBACnB,IAAI,SAAS,MAAM;AAAA,gBACnB,IAAI,SAAS,IAAI;AAAA,gBACjB,IAAI,SAAS,IAAI;AAAA,gBACjB,QAAQ;AAAA,gBACR,aAAa;AAAA,gBACb,iBAAgB;AAAA,gBAChB,eAAc;AAAA;AAAA,YAChB;AAAA,YACA,oBAAC,YAAO,IAAI,SAAS,MAAM,GAAG,IAAI,SAAS,MAAM,GAAG,GAAG,cAAc,MAAM,aAAa;AAAA,YACxF,oBAAC,YAAO,IAAI,SAAS,IAAI,GAAG,IAAI,SAAS,IAAI,GAAG,GAAG,cAAc,MAAM,aAAa;AAAA,aACtF;AAAA,WAEJ;AAAA,QAEA,oBAAC,SAAI,WAAU,2BACZ,oBAAU,IAAI,CAAC,aAAa;AAC3B,gBAAM,WAAW,QAAQ,SAAS,EAAE;AACpC,gBAAM,SACJ,aAAa,SACT,SACA,iBAAiB,EAAE,YAAY,SAAS,IAAI,SAAS,CAAC;AAC5D,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,KAAK,CAAC,YAAY,MAAM,aAAa,QAAQ,SAAS,EAAE,IAAI;AAAA,cAC5D,MAAK;AAAA,cACL;AAAA,cACA,eAAe,CAAC,UAAU,kBAAkB,OAAO,SAAS,EAAE;AAAA,cAC9D,SAAS,MAAM,aAAa,UAAa,YAAY,SAAS,EAAE;AAAA,cAChE,WAAW;AAAA,gBACT;AAAA,gBACA,aAAa,UAAa;AAAA,gBAC1B;AAAA,gBACA,QAAQ;AAAA,cACV;AAAA,cAEC,mBAAS;AAAA;AAAA,YAbL,SAAS;AAAA,UAchB;AAAA,QAEJ,CAAC,GACH;AAAA,QAEA,oBAAC,SAAI,WAAU,2BACZ,kBAAQ,IAAI,CAAC,WAAW;AACvB,gBAAM,gBAAgB,UAAU,OAAO,EAAE,OAAO,CAAC,UAAU,MAAM,aAAa,OAAO,EAAE;AACvF,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,KAAK,CAAC,YAAY,MAAM,WAAW,QAAQ,OAAO,EAAE,IAAI;AAAA,cACxD,MAAK;AAAA,cACL;AAAA,cACA,aAAa,CAAC,UAAU,gBAAgB,OAAO,OAAO,EAAE;AAAA,cACxD,WAAW;AAAA,gBACT;AAAA,gBACA,cAAc,SAAS,KAAK;AAAA,gBAC5B;AAAA,gBACA,cAAc,IAAI,CAAC,UAAU,iBAAiB,KAAK,GAAG,eAAe;AAAA,cACvE;AAAA,cAEC,iBAAO;AAAA;AAAA,YAZH,OAAO;AAAA,UAad;AAAA,QAEJ,CAAC,GACH;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-matchings",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A React component for matching questions and answers",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",