@nldoc/validation 1.0.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.
@@ -0,0 +1,388 @@
1
+ import { default as default_2 } from 'react';
2
+ import { Editor } from '@tiptap/react';
3
+ import { JSONContent } from '@tiptap/react';
4
+ import { PropsWithChildren } from 'react';
5
+ import { UseConversion } from '@nldoc/api';
6
+ import { ValidationFinding } from '@nldoc/types';
7
+
8
+ /**
9
+ * A Bundle represents multiple findings from the same block-level element.
10
+ * Bundles are created before stacking and can be stacked with other bundles/findings.
11
+ */
12
+ export declare interface Bundle {
13
+ id: string;
14
+ type: 'bundle';
15
+ findings: PositionedFinding[];
16
+ blockId: string;
17
+ rule: string;
18
+ desiredYLocation: number;
19
+ yLocation: number;
20
+ }
21
+
22
+ /**
23
+ * Rules that support bundling by block-level elements.
24
+ * These findings will be grouped by their parent block (paragraph, heading, table cell).
25
+ */
26
+ export declare const BUNDLEABLE_RULES: string[];
27
+
28
+ /**
29
+ * Stable sort comparator for positioned findings.
30
+ * Uses resource ID as tiebreaker to prevent position flip-flopping when findings have same Y location.
31
+ */
32
+ export declare function byIncreasingDesiredYLocation(a: PositionedFinding, b: PositionedFinding): number;
33
+
34
+ /**
35
+ * Calculates the desired Y location for a finding.
36
+ *
37
+ * @param finding - The validation finding to position
38
+ * @param config - Configuration options
39
+ * @returns The Y location in pixels, or null if element not found
40
+ */
41
+ export declare function calculateDesiredYLocation(finding: ValidationFinding, config?: PositioningConfig): number | null;
42
+
43
+ /**
44
+ * Groups findings by their parent block ID for bundleable rules.
45
+ * Only creates bundles for groups with 2+ findings.
46
+ *
47
+ * @param positionedFindings - Array of findings with position information
48
+ * @param findingBlockMap - Maps finding resourceId to parent block ID
49
+ * @returns Array of bundles, each containing findings from the same block
50
+ */
51
+ export declare function computeBundles(positionedFindings: PositionedFinding[], findingBlockMap: Map<string, string>): Bundle[];
52
+
53
+ /**
54
+ * Creates initial "stacks" from stackable items (bundles and findings).
55
+ * Each item starts as its own stack.
56
+ *
57
+ * @param items - Array of bundles and findings
58
+ * @returns Array of individual stacks (one per item)
59
+ */
60
+ export declare function createInitialStacksFromItems(items: StackableItem[]): Stack[];
61
+
62
+ /**
63
+ * Creates a set of finding resource IDs that are part of bundles.
64
+ * Used to exclude these findings from stacking.
65
+ *
66
+ * @param bundles - Array of bundles
67
+ * @returns Set of resource IDs that are bundled
68
+ */
69
+ export declare function getBundledFindingIds(bundles: Bundle[]): Set<string>;
70
+
71
+ /**
72
+ * Type guard to check if a stackable item is a bundle.
73
+ */
74
+ export declare function isBundle(item: StackableItem): item is Bundle;
75
+
76
+ export declare function makePositionedFinding(finding: ValidationFinding, config?: PositioningConfig): PositionedFinding;
77
+
78
+ export declare function makePositionedFindings(findings: ValidationFinding[], config?: PositioningConfig): PositionedFinding[];
79
+
80
+ /**
81
+ * Merges stacks based on whether their DESIRED positions would overlap.
82
+ * Works with stacks containing bundles and/or findings.
83
+ *
84
+ * @param stacks - Array of individual stacks
85
+ * @param itemHeights - Map of item ID to measured height (for bundles and findings)
86
+ * @param expandedStackIds - Set of stack IDs that should remain expanded
87
+ * @param previousStacks - Stacks from previous render for ID stability
88
+ * @returns Array of merged stacks
89
+ */
90
+ export declare function mergeOverlappingStacksWithBundles(stacks: Stack[], itemHeights: Map<string, number>, expandedStackIds?: Set<string>, previousStacks?: Stack[]): Stack[];
91
+
92
+ export declare interface PositionedFinding extends ValidationFinding {
93
+ desiredYLocation: number;
94
+ yLocation?: number;
95
+ hidden: boolean;
96
+ }
97
+
98
+ /**
99
+ * Configuration for positioning calculations
100
+ */
101
+ export declare interface PositioningConfig {
102
+ /**
103
+ * CSS selector for the editor container element
104
+ * @default '#tiptap'
105
+ */
106
+ editorSelector?: string;
107
+ }
108
+
109
+ /**
110
+ * Positions stacks to prevent overlaps using actual rendered heights.
111
+ *
112
+ * @param stacks - Array of stacks to position
113
+ * @param stackRefs - DOM refs for stack components
114
+ * @returns Array of stacks with adjusted yLocation
115
+ */
116
+ export declare function positionStacksWithBundles(stacks: Stack[], stackRefs: Record<string, HTMLDivElement | null>): Stack[];
117
+
118
+ /**
119
+ * A Stack represents one or more stackable items that should be displayed together.
120
+ * Stacks can contain any mix of bundles and individual findings of the same rule.
121
+ */
122
+ export declare interface Stack {
123
+ id: string;
124
+ type: 'stack';
125
+ items: StackableItem[];
126
+ rule: string;
127
+ isExpandable: boolean;
128
+ isExpanded: boolean;
129
+ desiredYLocation: number;
130
+ yLocation: number;
131
+ }
132
+
133
+ /**
134
+ * A stackable item can be either a bundle or an individual finding.
135
+ * Both are actionable in one click and can be grouped together.
136
+ */
137
+ export declare type StackableItem = Bundle | PositionedFinding;
138
+
139
+ /**
140
+ * Converts TipTap editor JSON to validation format with debouncing
141
+ *
142
+ * @param editorJSON - The TipTap editor JSON content
143
+ * @param options - Configuration options for validation
144
+ * @returns Conversion state from the API
145
+ */
146
+ export declare function useTiptapValidation(editorJSON: JSONContent | null, options?: ValidationHookOptions): UseConversion;
147
+
148
+ /**
149
+ * Validates TipTap document content by converting it to validation JSON.
150
+ * All derived values are synchronously memoized; no promises leave the hook.
151
+ *
152
+ * @param editorJSON - The TipTap editor JSON content to validate
153
+ * @param options - Configuration options for validation
154
+ * @returns Validation state including findings, loading state, and errors
155
+ *
156
+ * @example
157
+ * ```tsx
158
+ * const editor = useEditor({ extensions })
159
+ * const { isValidating, findings, error } = useValidation(editor?.getJSON() ?? null)
160
+ *
161
+ * useEffect(() => {
162
+ * if (findings.length > 0) {
163
+ * editor?.commands.setValidationFindings(findings)
164
+ * }
165
+ * }, [editor, findings])
166
+ * ```
167
+ */
168
+ export declare function useValidation(editorJSON: JSONContent | null, options?: ValidationHookOptions): Validation;
169
+
170
+ /**
171
+ * Hook to access validation state from context
172
+ * Must be used within a ValidationProvider
173
+ *
174
+ * @returns Current validation state
175
+ * @throws Error if used outside of ValidationProvider
176
+ *
177
+ * @example
178
+ * ```tsx
179
+ * function ValidationStatus() {
180
+ * const { findings, isValidating } = useValidationContext()
181
+ * return <div>{isValidating ? 'Validating...' : `${findings.length} issues`}</div>
182
+ * }
183
+ * ```
184
+ */
185
+ export declare function useValidationContext(): Validation;
186
+
187
+ export declare interface Validation {
188
+ isValidating: boolean;
189
+ findings: ValidationFinding[];
190
+ error: Error | null;
191
+ }
192
+
193
+ export declare const ValidationContext: default_2.Context<Validation>;
194
+
195
+ export declare interface ValidationHookOptions {
196
+ /**
197
+ * Delay in milliseconds before starting validation after editor changes
198
+ * @default 500
199
+ */
200
+ debounceDelay?: number;
201
+ /**
202
+ * Maximum wait time in milliseconds before forcing validation
203
+ * @default 5000
204
+ */
205
+ debounceMaxWait?: number;
206
+ /**
207
+ * Whether to include image assets in the validation request
208
+ * @default false
209
+ */
210
+ includeAssets?: boolean;
211
+ }
212
+
213
+ /**
214
+ * Provider component that manages validation state and applies findings to the editor
215
+ *
216
+ * Automatically tracks editor updates and validates content with debouncing.
217
+ * Excludes validation transactions to prevent infinite loops.
218
+ *
219
+ * @example
220
+ * Basic usage (automatic tracking):
221
+ * ```tsx
222
+ * const editor = useEditor({ extensions: [NLDocStarterKit] })
223
+ *
224
+ * <ValidationProvider editor={editor}>
225
+ * <EditorContent editor={editor} />
226
+ * <ValidationSidebar />
227
+ * </ValidationProvider>
228
+ * ```
229
+ *
230
+ * @example
231
+ * Advanced usage (manual control):
232
+ * ```tsx
233
+ * const editor = useEditor({ extensions: [NLDocStarterKit] })
234
+ * const [editorJSON, setEditorJSON] = useState(null)
235
+ *
236
+ * <ValidationProvider editor={editor} editorJSON={editorJSON}>
237
+ * <EditorContent editor={editor} />
238
+ * </ValidationProvider>
239
+ * ```
240
+ */
241
+ export declare function ValidationProvider({ children, editor, editorJSON: providedEditorJSON, options, autoApplyFindings, onApplyFindings, }: ValidationProviderProps): default_2.JSX.Element;
242
+
243
+ declare interface ValidationProviderProps extends PropsWithChildren {
244
+ /**
245
+ * The TipTap editor instance (optional if editorJSON is provided)
246
+ */
247
+ editor?: Editor | null;
248
+ /**
249
+ * The editor JSON content to validate (optional)
250
+ * If not provided, ValidationProvider will automatically track editor updates internally,
251
+ * excluding transactions from setValidationFindings to prevent infinite loops.
252
+ * Only provide this if you need custom control over which updates trigger validation.
253
+ */
254
+ editorJSON?: JSONContent | null;
255
+ /**
256
+ * Validation configuration options
257
+ */
258
+ options?: ValidationHookOptions;
259
+ /**
260
+ * Whether to automatically apply findings to the editor
261
+ * @default true
262
+ */
263
+ autoApplyFindings?: boolean;
264
+ /**
265
+ * Callback to apply findings to the editor
266
+ * By default, this will call editor.commands.setValidationFindings if available
267
+ */
268
+ onApplyFindings?: (editor: Editor, findings: ValidationFinding[]) => void;
269
+ }
270
+
271
+ export declare const ValidationResult: default_2.FC<ValidationResultProps>;
272
+
273
+ /**
274
+ * Component for displaying accessibility validation results.
275
+ * Handles both single findings, bundles, and collapsed stacks.
276
+ *
277
+ * This component:
278
+ * - Shows a toggle button that expands or collapses the detailed message
279
+ * - Handles resolution actions through onResolve callback
280
+ * - Closes on outside clicks and Escape key presses (default variant)
281
+ * - Uses `aria-controls`, `aria-expanded`, and `aria-live` attributes for accessibility
282
+ *
283
+ * @param findings - Array of validation findings (single finding or bundle)
284
+ * @param isBundle - Whether this is a bundle of findings (affects message formatting)
285
+ * @param variant - Display variant: 'default' for normal display, 'stack' for collapsed stack preview
286
+ * @param stackAmount - Number to display in count badge (stack variant only)
287
+ * @param stackIsExpanded - Whether the stack is expanded (stack variant only)
288
+ * @param stackOnToggle - Toggle callback for stack (stack variant only)
289
+ * @param onResolve - Callback when user triggers resolution for findings
290
+ * @param onRetrigger - Callback to retrigger validation
291
+ * @param isExpanded - Whether the result is expanded (default variant)
292
+ * @param onToggle - Toggle callback (default variant)
293
+ * @param labels - Text labels for UI elements
294
+ * @param renderIcon - Function to render icons
295
+ * @param renderMessage - Function to render finding message
296
+ * @param renderResolution - Function to render resolution component
297
+ */
298
+ export declare interface ValidationResultProps {
299
+ findings: ValidationFinding[];
300
+ isBundle?: boolean;
301
+ variant?: 'default' | 'stack';
302
+ stackAmount?: number;
303
+ stackIsExpanded?: boolean;
304
+ stackOnToggle?: () => void;
305
+ onResolve?: (ids: string[]) => void;
306
+ onRetrigger?: () => void;
307
+ isExpanded?: boolean;
308
+ onToggle?: () => void;
309
+ labels?: {
310
+ toggleFinding?: string;
311
+ toggleStackOpen?: string;
312
+ toggleStackClosed?: string;
313
+ severity?: {
314
+ error?: string;
315
+ warning?: string;
316
+ suggestion?: string;
317
+ };
318
+ };
319
+ renderIcon?: (type: 'error' | 'warning' | 'suggestion' | 'close' | 'chevron', props?: {
320
+ size?: 'small' | 'medium' | 'large';
321
+ }) => default_2.ReactNode;
322
+ renderMessage?: (finding: ValidationFinding, count: number, isBundle: boolean) => default_2.ReactNode;
323
+ renderResolution?: (findingIds: string[], onResolved: () => void) => default_2.ReactNode;
324
+ className?: string;
325
+ styles?: {
326
+ outerWrapper?: string;
327
+ toggleButton?: string;
328
+ toggleButtonSelected?: string;
329
+ dropShadow?: string;
330
+ amount?: string;
331
+ result?: string;
332
+ resultSeverity?: string;
333
+ resultSelected?: string;
334
+ label?: string;
335
+ message?: string;
336
+ messageWrapper?: string;
337
+ openValidations?: string;
338
+ resolution?: string;
339
+ stack?: string;
340
+ stackDecorationTwoItems?: string;
341
+ stackDecorationManyItems?: string;
342
+ };
343
+ }
344
+
345
+ /**
346
+ * Main component for displaying validation results summary with stacking and bundling.
347
+ *
348
+ * This component orchestrates the complex layout calculation process through multiple phases:
349
+ * 1. MEASURING: Render findings invisibly to measure their heights
350
+ * 2. BUNDLING: Group findings by parent block (paragraph, heading, etc.)
351
+ * 3. MEASURING_BUNDLES: Render bundles invisibly to measure their heights
352
+ * 4. STACKING: Create initial vertical stacks for overlapping items
353
+ * 5. MERGING: Merge overlapping stacks of the same rule
354
+ * 6. POSITIONING: Calculate final positions to prevent overlaps
355
+ * 7. IDLE: Render the final layout
356
+ */
357
+ export declare const ValidationResultSummary: default_2.FC<ValidationResultSummaryProps>;
358
+
359
+ export declare interface ValidationResultSummaryProps {
360
+ /** Validation findings to display */
361
+ findings: ValidationFinding[];
362
+ /** Whether the editor is animating (used to delay layout updates) */
363
+ isAnimating?: boolean;
364
+ /** Configuration for positioning calculations */
365
+ positioningConfig?: PositioningConfig;
366
+ /** Callback to build a map from finding resource IDs to their parent block IDs */
367
+ buildFindingBlockMap?: (findings: ValidationFinding[]) => Map<string, string>;
368
+ /** Props to pass to ValidationResult components */
369
+ validationResultProps?: Partial<ValidationResultProps>;
370
+ /** Custom component to render instead of ValidationResult (receives all ValidationResultProps) */
371
+ resultComponent?: default_2.ComponentType<ValidationResultProps>;
372
+ /** Custom component to render collapse button for expanded stacks */
373
+ collapseButtonComponent?: default_2.ComponentType<{
374
+ onClick: () => void;
375
+ }>;
376
+ /** Debounce delay for window resize in ms */
377
+ resizeDebounceDelay?: number;
378
+ /** CSS class name for the container */
379
+ className?: string;
380
+ /** Styles for the container and child elements */
381
+ styles?: {
382
+ container?: string;
383
+ comment?: string;
384
+ findingInExpandedStack?: string;
385
+ };
386
+ }
387
+
388
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const Re=require("@nldoc/types"),b=require("react"),ve=require("zod"),J=require("@nldoc/api"),Te=require("use-debounce"),xe=require("@nldoc/tiptap-extensions");function de(n,t={}){const{debounceDelay:r=500,debounceMaxWait:o=5e3,includeAssets:s=!1}=t,[i]=Te.useDebounce(n,r,{maxWait:o}),l=b.useMemo(()=>{if(!i)return null;const p=s?i:{...i,attrs:{...i.attrs,assets:{}}};return new Blob([JSON.stringify(p)],{type:J.ContentType.Tiptap})},[i,s]);return J.useConversion(l,J.ContentType.Validation)}const _e=new Error("An unknown error occurred"),Ne=ve.z.array(Re.ValidationFinding);function ue(n,t={}){const r=de(n,t),[o,s]=b.useState(!1),[i,l]=b.useState([]),[p,k]=b.useState(null);return b.useEffect(()=>{if(r.status==="error"){s(!1),k(r.error);return}if(r.status==="idle"){s(!1);return}if(r.status!=="done"){s(!0);return}try{const E=Ne.parse(JSON.parse(r.content));l(E),k(null)}catch(E){k(E instanceof Error?E:_e)}finally{s(!1)}},[r]),{isValidating:o,findings:i,error:p}}var V={exports:{}},P={};var re;function ye(){if(re)return P;re=1;var n=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function r(o,s,i){var l=null;if(i!==void 0&&(l=""+i),s.key!==void 0&&(l=""+s.key),"key"in s){i={};for(var p in s)p!=="key"&&(i[p]=s[p])}else i=s;return s=i.ref,{$$typeof:n,type:o,key:l,ref:s!==void 0?s:null,props:i}}return P.Fragment=t,P.jsx=r,P.jsxs=r,P}var B={};var se;function Ae(){return se||(se=1,process.env.NODE_ENV!=="production"&&(function(){function n(e){if(e==null)return null;if(typeof e=="function")return e.$$typeof===Q?null:e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case A:return"Fragment";case S:return"Profiler";case c:return"StrictMode";case N:return"Suspense";case Z:return"SuspenseList";case H:return"Activity"}if(typeof e=="object")switch(typeof e.tag=="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),e.$$typeof){case m:return"Portal";case u:return e.displayName||"Context";case h:return(e._context.displayName||"Context")+".Consumer";case O:var f=e.render;return e=e.displayName,e||(e=f.displayName||f.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case D:return f=e.displayName||null,f!==null?f:n(e.type)||"Memo";case U:f=e._payload,e=e._init;try{return n(e(f))}catch{}}return null}function t(e){return""+e}function r(e){try{t(e);var f=!1}catch{f=!0}if(f){f=console;var I=f.error,T=typeof Symbol=="function"&&Symbol.toStringTag&&e[Symbol.toStringTag]||e.constructor.name||"Object";return I.call(f,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",T),t(e)}}function o(e){if(e===A)return"<>";if(typeof e=="object"&&e!==null&&e.$$typeof===U)return"<...>";try{var f=n(e);return f?"<"+f+">":"<...>"}catch{return"<...>"}}function s(){var e=M.A;return e===null?null:e.getOwner()}function i(){return Error("react-stack-top-frame")}function l(e){if(G.call(e,"key")){var f=Object.getOwnPropertyDescriptor(e,"key").get;if(f&&f.isReactWarning)return!1}return e.key!==void 0}function p(e,f){function I(){j||(j=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",f))}I.isReactWarning=!0,Object.defineProperty(e,"key",{get:I,configurable:!0})}function k(){var e=n(this.type);return $[e]||($[e]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),e=this.props.ref,e!==void 0?e:null}function E(e,f,I,T,F,W){var x=I.ref;return e={$$typeof:v,type:e,key:f,props:I,_owner:T},(x!==void 0?x:null)!==null?Object.defineProperty(e,"ref",{enumerable:!1,get:k}):Object.defineProperty(e,"ref",{enumerable:!1,value:null}),e._store={},Object.defineProperty(e._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(e,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(e,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:F}),Object.defineProperty(e,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:W}),Object.freeze&&(Object.freeze(e.props),Object.freeze(e)),e}function a(e,f,I,T,F,W){var x=f.children;if(x!==void 0)if(T)if(Y(x)){for(T=0;T<x.length;T++)g(x[T]);Object.freeze&&Object.freeze(x)}else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");else g(x);if(G.call(f,"key")){x=n(e);var w=Object.keys(f).filter(function(ke){return ke!=="key"});T=0<w.length?"{key: someKey, "+w.join(": ..., ")+": ...}":"{key: someKey}",te[x+T]||(w=0<w.length?"{"+w.join(": ..., ")+": ...}":"{}",console.error(`A props object containing a "key" prop is being spread into JSX:
2
+ let props = %s;
3
+ <%s {...props} />
4
+ React keys must be passed directly to JSX without using spread:
5
+ let props = %s;
6
+ <%s key={someKey} {...props} />`,T,x,w,x),te[x+T]=!0)}if(x=null,I!==void 0&&(r(I),x=""+I),l(f)&&(r(f.key),x=""+f.key),"key"in f){I={};for(var q in f)q!=="key"&&(I[q]=f[q])}else I=f;return x&&p(I,typeof e=="function"?e.displayName||e.name||"Unknown":e),E(e,x,I,s(),F,W)}function g(e){_(e)?e._store&&(e._store.validated=1):typeof e=="object"&&e!==null&&e.$$typeof===U&&(e._payload.status==="fulfilled"?_(e._payload.value)&&e._payload.value._store&&(e._payload.value._store.validated=1):e._store&&(e._store.validated=1))}function _(e){return typeof e=="object"&&e!==null&&e.$$typeof===v}var R=b,v=Symbol.for("react.transitional.element"),m=Symbol.for("react.portal"),A=Symbol.for("react.fragment"),c=Symbol.for("react.strict_mode"),S=Symbol.for("react.profiler"),h=Symbol.for("react.consumer"),u=Symbol.for("react.context"),O=Symbol.for("react.forward_ref"),N=Symbol.for("react.suspense"),Z=Symbol.for("react.suspense_list"),D=Symbol.for("react.memo"),U=Symbol.for("react.lazy"),H=Symbol.for("react.activity"),Q=Symbol.for("react.client.reference"),M=R.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,G=Object.prototype.hasOwnProperty,Y=Array.isArray,L=console.createTask?console.createTask:function(){return null};R={react_stack_bottom_frame:function(e){return e()}};var j,$={},ee=R.react_stack_bottom_frame.bind(R,i)(),ne=L(o(i)),te={};B.Fragment=A,B.jsx=function(e,f,I){var T=1e4>M.recentlyCreatedOwnerStacks++;return a(e,f,I,!1,T?Error("react-stack-top-frame"):ee,T?L(o(e)):ne)},B.jsxs=function(e,f,I){var T=1e4>M.recentlyCreatedOwnerStacks++;return a(e,f,I,!0,T?Error("react-stack-top-frame"):ee,T?L(o(e)):ne)}})()),B}var oe;function Le(){return oe||(oe=1,process.env.NODE_ENV==="production"?V.exports=ye():V.exports=Ae()),V.exports}var d=Le();const z=b.createContext({findings:[],isValidating:!1,error:null});function je({children:n,editor:t,editorJSON:r,options:o,autoApplyFindings:s=!0,onApplyFindings:i}){const[l,p]=b.useState(()=>r!==void 0?null:t?.getJSON()??null);b.useEffect(()=>{if(!t||r!==void 0)return;const a=({editor:g,transaction:_})=>{_.getMeta(xe.TransactionMeta.blockValidation)!==!0&&p(g.getJSON())};return t.on("update",a),()=>{t.off("update",a)}},[t,r]);const E=ue(r??l,o);return b.useEffect(()=>{if(!(!t||!s))if(i)i(t,E.findings);else{const a=t.commands;a.setValidationFindings&&a.setValidationFindings(E.findings)}},[t,E.findings,s,i]),d.jsx(z.Provider,{value:E,children:n})}function Oe(){return b.useContext(z)}const le=({findings:n,isBundle:t=!1,variant:r="default",stackAmount:o,stackIsExpanded:s,stackOnToggle:i,onResolve:l,onRetrigger:p,isExpanded:k,onToggle:E,labels:a={},renderIcon:g,renderMessage:_,renderResolution:R,className:v="",styles:m={}})=>{const A=b.useRef(null),[c,S]=b.useState(!1),h=r==="stack";if(h&&(o===void 0||s===void 0||!i))return console.error('ValidationResult: stackAmount, stackIsExpanded, and stackOnToggle are required when variant="stack"'),null;const u=h?s:k??c,O=h?i:E??(()=>S(!c)),N=n[0],D=g??(L=>{const j={error:"⚠️",warning:"⚠️",suggestion:"ℹ️",close:"✕",chevron:"▼"};return d.jsx("span",{children:j[L]})}),H=_??((L,j,$)=>$?`${j} ${L.rule} findings`:j>1?`${j} ${L.rule} findings`:L.rule),M=R??((L,j)=>d.jsx("button",{onClick:()=>{l&&l(L),j()},children:"Resolve"})),G=()=>{p&&p()},Y=d.jsxs("div",{className:`${m.outerWrapper??""} ${v}`,ref:h?null:A,children:[d.jsx("button",{className:`${m.toggleButton??""} ${u?m.toggleButtonSelected??"":""}`,onClick:O,"aria-controls":`comment-${t?"":"inner-"}${N.resourceId}`,"aria-expanded":u,"aria-label":a.toggleFinding??"Toggle finding details","data-testid":"toggle-button",children:d.jsx("div",{className:m.dropShadow??"",children:h?d.jsx("div",{className:m.amount??"",children:d.jsx("strong",{children:o})}):D(u?"close":N.severity)})}),d.jsxs("div",{className:`${m.result??""} ${m.resultSeverity?`${m.resultSeverity}-${N.severity}`:""} ${u?m.resultSelected??"":""}`,id:`comment-${t?"":"inner-"}${N.resourceId}`,tabIndex:-1,"aria-live":"polite",children:[d.jsxs("div",{className:m.messageWrapper??"",children:[d.jsxs("div",{className:m.label??"",children:[D(N.severity),d.jsx("strong",{children:a.severity?.[N.severity]??N.severity})]}),d.jsx("div",{className:m.message??"",children:H(N,h?o??0:n.length,t)})]}),h?d.jsx("div",{className:m.openValidations??"",children:d.jsx("button",{onClick:i,children:a.toggleStackOpen??"Open validations"})}):d.jsx("div",{className:m.resolution??"",children:M(n.map(L=>L.resourceId),G)})]})]});return h?d.jsxs("div",{className:m.stack??"",children:[Y,o===2?d.jsx("div",{className:m.stackDecorationTwoItems??""}):d.jsx("div",{className:m.stackDecorationManyItems??"",children:d.jsx("div",{className:m.stackDecorationManyItems??""})})]}):Y},fe=["no-underline","style-bold-in-header","style-italic-in-header"];function pe(n,t){const r=new Map;for(const s of n){if(!fe.includes(s.rule)||s.hidden)continue;const i=t.get(s.resourceId);if(!i)continue;const l=r.get(i);l?l.push(s):r.set(i,[s])}const o=[];for(const[s,i]of r){if(i.length<2)continue;const l=[...i].sort((p,k)=>p.desiredYLocation-k.desiredYLocation);o.push({id:crypto.randomUUID(),type:"bundle",findings:l,blockId:s,rule:l[0].rule,desiredYLocation:l[0].desiredYLocation,yLocation:l[0].desiredYLocation})}return o}function me(n){const t=new Set;for(const r of n)for(const o of r.findings)t.add(o.resourceId);return t}function ge(n,t={}){const{editorSelector:r="#tiptap"}=t,o=document.getElementById(n.resourceId);if(!o)return null;const s=document.querySelector(r);if(!s)throw new Error(`Editor not found: ${r} element does not exist`);const i=s.getBoundingClientRect().top;return o.getBoundingClientRect().top-i}function Ee(n,t){const o=ge(n,t)??0;return{...n,desiredYLocation:o,yLocation:o,hidden:!1}}function K(n,t){return n.map(r=>Ee(r,t)).sort(Se)}function Se(n,t){const r=n.desiredYLocation-t.desiredYLocation;return Math.abs(r)>.01?r:n.resourceId.localeCompare(t.resourceId)}function X(n){return"type"in n&&n.type==="bundle"}function ie(n){return X(n)?`bundle:${n.blockId}`:n.resourceId}function C(n){return n.desiredYLocation}function he(n){return n.map(t=>({id:crypto.randomUUID(),type:"stack",items:[t],rule:t.rule,isExpandable:!1,isExpanded:!1,desiredYLocation:C(t),yLocation:C(t)}))}function Ie(n,t,r=new Set,o=[]){if(n.length===0)return[];const s=[...n].sort((_,R)=>_.desiredYLocation-R.desiredYLocation),i=[...o],l=[];let p=[],k=null,E=0,a=0,g=0;for(const _ of s){const R=_.items[0],v=R.rule,m=C(R),A=Ce(R,t),c=Math.max(m,g);if(p.length===0){p=[R],k=v,E=c,a=A,g=c+A;continue}const S=v===k,h=c<=E+a;S&&h?(p.push(R),g=E+a):(l.push(ae(p,r,i)),p=[R],k=v,E=c,a=A,g=c+A)}return p.length>0&&l.push(ae(p,r,i)),l}function Ce(n,t){return X(n)?t.get(n.id)??0:t.get(n.resourceId)??0}function ae(n,t,r){const o=[...n].sort((l,p)=>C(l)-C(p)),s=we(o,r),i=s?s.id:crypto.randomUUID();return{id:i,type:"stack",items:o,rule:o[0].rule,isExpandable:o.length>1,isExpanded:t.has(i),desiredYLocation:C(o[0]),yLocation:C(o[0])}}function we(n,t){if(t.length===0)return null;const r=new Set(n.map(ie));let o=null,s=0,i=-1;for(let l=0;l<t.length;l++){const p=t[l],k=new Set(p.items.map(ie));let E=0;for(const g of r)k.has(g)&&E++;const a=E/Math.max(r.size,k.size);a>s&&(s=a,o=p,i=l)}return s>=.5&&i>=0?(t.splice(i,1),o):null}function be(n,t){const r=n.map(o=>({...o}));for(let o=1;o<r.length;o++){const s=r[o-1],i=r[o],l=t[s.id]?.offsetHeight??0,p=s.yLocation+l;i.yLocation=Math.max(i.desiredYLocation,p)}return r}const y={IDLE:"idle",MEASURING:"measuring",BUNDLING:"bundling",MEASURING_BUNDLES:"measuring-bundles",STACKING:"stacking",MERGING:"merging",POSITIONING:"positioning"},De={phase:y.IDLE,bundles:[],stacks:[],previousStacks:[],expandedStackIds:new Set,itemHeights:new Map,findingBlockMap:new Map,findings:[]},ce=(n,t)=>({...n,phase:y.MEASURING,findings:t,previousStacks:n.stacks,stacks:[],bundles:[]});function Me(n,t){switch(t.type){case"FINDINGS_UPDATED":case"ANIMATION_FINISHED":return n.findings===t.findings?n:ce(n,t.findings);case"RECALCULATE":return ce(n,n.findings);case"HEIGHTS_MEASURED":return{...n,phase:y.BUNDLING,itemHeights:t.heights};case"BLOCK_MAP_BUILT":return{...n,findingBlockMap:t.blockMap};case"BUNDLES_CREATED":return{...n,phase:y.MEASURING_BUNDLES,bundles:t.bundles};case"BUNDLE_HEIGHTS_MEASURED":return{...n,phase:y.STACKING,itemHeights:new Map([...n.itemHeights,...t.heights])};case"STACKS_CREATED":return{...n,phase:y.MERGING,stacks:t.stacks};case"STACKS_MERGED":return{...n,phase:y.POSITIONING,stacks:t.stacks};case"STACKS_POSITIONED":return{...n,phase:y.IDLE,stacks:t.stacks};case"TOGGLE_STACK":{const r=new Set(n.expandedStackIds);return r.has(t.stackId)?r.delete(t.stackId):r.add(t.stackId),{...n,expandedStackIds:r,phase:y.STACKING,previousStacks:n.stacks}}default:return n}}function Pe(n,t){let r;return()=>{clearTimeout(r),r=setTimeout(n,t)}}const Be=({findings:n,isAnimating:t=!1,positioningConfig:r,buildFindingBlockMap:o,validationResultProps:s={},resultComponent:i,collapseButtonComponent:l,resizeDebounceDelay:p=100,className:k="",styles:E={}})=>{const[a,g]=b.useReducer(Me,De),_=i??le,R=l,v=b.useRef({findings:{},bundles:{},stacks:{}}),m=b.useCallback(()=>g({type:"RECALCULATE"}),[]);b.useEffect(()=>{t||g({type:"ANIMATION_FINISHED",findings:n})},[t,n]),b.useEffect(()=>{g({type:"FINDINGS_UPDATED",findings:n}),g(o?{type:"BLOCK_MAP_BUILT",blockMap:o(n)}:{type:"BLOCK_MAP_BUILT",blockMap:new Map})},[n,o]),b.useEffect(()=>{const c=Pe(m,p);return window.addEventListener("resize",c),()=>window.removeEventListener("resize",c)},[m,p]),b.useLayoutEffect(()=>{const c=K(a.findings,r);switch(a.phase){case y.MEASURING:{const S=new Map;c.forEach(h=>{const u=v.current.findings[h.resourceId];u&&S.set(h.resourceId,u.offsetHeight)}),g({type:"HEIGHTS_MEASURED",heights:S});break}case y.BUNDLING:{const S=pe(c,a.findingBlockMap);g({type:"BUNDLES_CREATED",bundles:S});break}case y.MEASURING_BUNDLES:{const S=new Map;a.bundles.forEach(h=>{const u=v.current.bundles[h.id];u&&S.set(h.id,u.offsetHeight)}),g({type:"BUNDLE_HEIGHTS_MEASURED",heights:S});break}case y.STACKING:{const S=me(a.bundles),h=c.filter(O=>!S.has(O.resourceId)),u=he([...a.bundles,...h]);g({type:"STACKS_CREATED",stacks:u});break}case y.MERGING:{const S=Ie(a.stacks,a.itemHeights,a.expandedStackIds,a.previousStacks);g({type:"STACKS_MERGED",stacks:S});break}case y.POSITIONING:{const S=be(a.stacks,v.current.stacks);g({type:"STACKS_POSITIONED",stacks:S});break}}},[a.phase,a.findings,a.bundles,a.stacks,a.findingBlockMap,a.itemHeights,a.expandedStackIds,a.previousStacks,r]);const A={position:"absolute",visibility:"hidden",pointerEvents:"none"};return d.jsxs("section",{className:`${E.container??""} ${k}`,tabIndex:-1,"data-testid":"validation-results-summary",children:[a.phase===y.MEASURING&&K(a.findings,r).map(c=>d.jsx("div",{ref:S=>{v.current.findings[c.resourceId]=S},style:A,children:d.jsx(_,{...s,findings:[c],onRetrigger:m})},c.resourceId)),a.phase===y.MEASURING_BUNDLES&&a.bundles.map(c=>d.jsx("div",{className:E.comment??"",ref:S=>{v.current.bundles[c.id]=S},style:A,children:d.jsx(_,{...s,findings:c.findings,onRetrigger:m,isBundle:!0})},c.id)),a.stacks.map(c=>{const S=c.items.length>1,h=c.items.flatMap(u=>"type"in u&&u.type==="bundle"?u.findings:[u]);return d.jsx("div",{"data-testid":`comment-item-${c.id}`,className:E.comment??"",ref:u=>{v.current.stacks[c.id]=u},style:{top:`${c.yLocation}px`,position:"absolute"},children:S&&!c.isExpanded?d.jsxs(d.Fragment,{children:[c.items.map(u=>"type"in u&&u.type==="bundle"?d.jsx("div",{ref:N=>{v.current.bundles[u.id]=N},style:A,children:d.jsx(_,{...s,findings:u.findings,onRetrigger:m,isBundle:!0})},u.id):d.jsx("div",{ref:N=>{v.current.findings[u.resourceId]=N},style:A,children:d.jsx(_,{...s,findings:[u],onRetrigger:m})},u.resourceId)),d.jsx(_,{...s,variant:"stack",findings:h,stackAmount:c.items.length,stackIsExpanded:!1,stackOnToggle:()=>g({type:"TOGGLE_STACK",stackId:c.id}),onRetrigger:m})]}):d.jsxs(d.Fragment,{children:[c.items.map(u=>"type"in u&&u.type==="bundle"?d.jsx("div",{ref:N=>{v.current.bundles[u.id]=N},className:c.isExpanded?E.findingInExpandedStack:void 0,children:d.jsx(_,{...s,findings:u.findings,onRetrigger:m,isBundle:!0})},u.id):d.jsx("div",{ref:N=>{v.current.findings[u.resourceId]=N},className:c.isExpanded?E.findingInExpandedStack:void 0,children:d.jsx(_,{...s,findings:[u],onRetrigger:m})},u.resourceId)),S&&c.isExpanded&&R&&d.jsx(R,{onClick:()=>g({type:"TOGGLE_STACK",stackId:c.id})}),S&&c.isExpanded&&!R&&d.jsx("div",{children:d.jsx("button",{onClick:()=>g({type:"TOGGLE_STACK",stackId:c.id}),children:s.labels?.toggleStackClosed??"Collapse"})})]})},c.id)})]})};exports.BUNDLEABLE_RULES=fe;exports.ValidationContext=z;exports.ValidationProvider=je;exports.ValidationResult=le;exports.ValidationResultSummary=Be;exports.byIncreasingDesiredYLocation=Se;exports.calculateDesiredYLocation=ge;exports.computeBundles=pe;exports.createInitialStacksFromItems=he;exports.getBundledFindingIds=me;exports.isBundle=X;exports.makePositionedFinding=Ee;exports.makePositionedFindings=K;exports.mergeOverlappingStacksWithBundles=Ie;exports.positionStacksWithBundles=be;exports.useTiptapValidation=de;exports.useValidation=ue;exports.useValidationContext=Oe;