@byline/richtext-lexical 5.2.0 → 6.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.
- package/dist/config.d.ts +1 -1
- package/dist/field/apply-value-plugin.js +31 -6
- package/dist/field/capabilities/filter-transformers.d.ts +29 -0
- package/dist/field/capabilities/filter-transformers.js +8 -0
- package/dist/field/capabilities/registered-node-types.d.ts +29 -0
- package/dist/field/capabilities/registered-node-types.js +7 -0
- package/dist/field/config/built-in-extension-names.d.ts +42 -0
- package/dist/field/config/built-in-extension-names.js +12 -2
- package/dist/field/config/default-extensions.js +4 -0
- package/dist/field/config/default.js +8 -8
- package/dist/field/config/editor-config-context.d.ts +2 -2
- package/dist/field/config/editor-config-context.js +10 -13
- package/dist/field/config/resolve-editor-config.js +3 -3
- package/dist/field/config/root-dependencies.d.ts +26 -0
- package/dist/field/config/root-dependencies.js +8 -0
- package/dist/field/config/types.d.ts +61 -8
- package/dist/field/editor-context.js +44 -27
- package/dist/field/editor.js +15 -8
- package/dist/field/extensions/core-nodes/core-nodes-extension.d.ts +24 -0
- package/dist/field/extensions/core-nodes/core-nodes-extension.js +11 -0
- package/dist/field/extensions/heading/heading-extension.d.ts +20 -0
- package/dist/field/extensions/heading/heading-extension.js +9 -0
- package/dist/field/extensions/quote/quote-extension.d.ts +16 -0
- package/dist/field/extensions/quote/quote-extension.js +9 -0
- package/dist/field/hooks/use-markdown-toggle.d.ts +15 -0
- package/dist/field/hooks/use-markdown-toggle.js +12 -4
- package/dist/field/hooks/use-platform-modifier.d.ts +16 -0
- package/dist/field/hooks/use-platform-modifier.js +8 -0
- package/dist/field/markdown/transformers.d.ts +1 -0
- package/dist/field/markdown/transformers.js +5 -4
- package/dist/field/nodes/index.d.ts +11 -1
- package/dist/field/nodes/index.js +2 -2
- package/dist/field/normalize/adapted-notice.css +34 -0
- package/dist/field/normalize/adapted-notice.d.ts +29 -0
- package/dist/field/normalize/adapted-notice.js +35 -0
- package/dist/field/normalize/content-labels.d.ts +15 -0
- package/dist/field/normalize/content-labels.js +19 -0
- package/dist/field/normalize/declared-conversions.d.ts +131 -0
- package/dist/field/normalize/declared-conversions.js +93 -0
- package/dist/field/normalize/normalization-status.d.ts +33 -0
- package/dist/field/normalize/normalization-status.js +28 -0
- package/dist/field/normalize/normalize-value.d.ts +38 -0
- package/dist/field/normalize/normalize-value.js +132 -0
- package/dist/field/normalize/use-notice-text.d.ts +11 -0
- package/dist/field/normalize/use-notice-text.js +34 -0
- package/dist/field/plugins/toolbar-plugin/index.d.ts +23 -0
- package/dist/field/plugins/toolbar-plugin/index.js +104 -78
- package/dist/field/test-support/build-test-editor.js +49 -0
- package/dist/field/test-support/capture-editor.js +7 -0
- package/dist/field/test-support/frame-control.js +40 -0
- package/dist/field/test-support/jsdom-setup.js +21 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +6 -1
- package/dist/scan/capability-manifest.d.ts +30 -0
- package/dist/scan/capability-manifest.js +43 -0
- package/dist/scan/scan-documents.d.ts +29 -0
- package/dist/scan/scan-documents.js +39 -0
- package/dist/scan/types.d.ts +63 -0
- package/dist/scan/types.js +1 -0
- package/dist/scan.d.ts +25 -0
- package/dist/scan.js +1 -0
- package/package.json +14 -5
- package/src/config.ts +1 -1
- package/src/field/apply-value-plugin.tsx +71 -5
- package/src/field/capabilities/filter-transformers.test.tsx +71 -0
- package/src/field/capabilities/filter-transformers.ts +42 -0
- package/src/field/capabilities/markdown-behaviour.test.tsx +188 -0
- package/src/field/capabilities/no-blanket-registration.test.tsx +86 -0
- package/src/field/capabilities/registered-node-types.test.tsx +29 -0
- package/src/field/capabilities/registered-node-types.ts +36 -0
- package/src/field/config/built-in-extension-names.test.node.ts +38 -14
- package/src/field/config/built-in-extension-names.ts +29 -1
- package/src/field/config/default-extensions.ts +6 -0
- package/src/field/config/default.ts +8 -8
- package/src/field/config/editor-config-context.tsx +14 -9
- package/src/field/config/list-removal.test.tsx +44 -0
- package/src/field/config/resolve-editor-config.test.node.ts +27 -6
- package/src/field/config/resolve-editor-config.ts +3 -3
- package/src/field/config/root-dependencies.ts +32 -0
- package/src/field/config/types.ts +61 -17
- package/src/field/editor-component.test.tsx +18 -3
- package/src/field/editor-context.regression.test.tsx +124 -0
- package/src/field/editor-context.tsx +90 -22
- package/src/field/editor.tsx +22 -12
- package/src/field/extensions/core-nodes/core-nodes-extension.test.tsx +13 -0
- package/src/field/extensions/core-nodes/core-nodes-extension.ts +32 -0
- package/src/field/extensions/heading/heading-extension.test.tsx +32 -0
- package/src/field/extensions/heading/heading-extension.ts +27 -0
- package/src/field/extensions/quote/quote-extension.ts +23 -0
- package/src/field/hooks/markdown-source-mode.test.tsx +221 -0
- package/src/field/hooks/use-markdown-toggle.ts +39 -4
- package/src/field/hooks/use-platform-modifier.test.tsx +81 -0
- package/src/field/hooks/use-platform-modifier.ts +41 -0
- package/src/field/markdown/transformers.ts +18 -4
- package/src/field/nodes/index.ts +11 -1
- package/src/field/normalize/adapted-notice.css +52 -0
- package/src/field/normalize/adapted-notice.tsx +63 -0
- package/src/field/normalize/clipboard-equivalence.test.tsx +330 -0
- package/src/field/normalize/content-labels.ts +44 -0
- package/src/field/normalize/declared-conversions.test.node.ts +67 -0
- package/src/field/normalize/declared-conversions.ts +160 -0
- package/src/field/normalize/emission-gate.test.tsx +302 -0
- package/src/field/normalize/normalization-status.tsx +48 -0
- package/src/field/normalize/normalize-load.test.tsx +371 -0
- package/src/field/normalize/normalize-value.test.node.ts +530 -0
- package/src/field/normalize/normalize-value.ts +180 -0
- package/src/field/normalize/notice-i18n.test.tsx +128 -0
- package/src/field/normalize/use-notice-text.ts +82 -0
- package/src/field/normalize/wiring.test.tsx +348 -0
- package/src/field/plugins/toolbar-plugin/block-format-capabilities.test.tsx +57 -0
- package/src/field/plugins/toolbar-plugin/index.tsx +131 -68
- package/src/field/test-support/build-test-editor.test.tsx +17 -0
- package/src/field/test-support/build-test-editor.ts +99 -0
- package/src/field/test-support/capture-editor.tsx +20 -0
- package/src/field/test-support/frame-control.ts +91 -0
- package/src/field/test-support/jsdom-setup.ts +41 -0
- package/src/index.ts +12 -1
- package/src/scan/capability-manifest.test.tsx +82 -0
- package/src/scan/capability-manifest.ts +81 -0
- package/src/scan/scan-documents.test.node.ts +125 -0
- package/src/scan/scan-documents.ts +78 -0
- package/src/scan/types.ts +66 -0
- package/src/scan.ts +27 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { act } from 'react'
|
|
2
|
+
|
|
3
|
+
import { defineAdminConfig } from '@byline/core'
|
|
4
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
|
5
|
+
import {
|
|
6
|
+
$createParagraphNode,
|
|
7
|
+
$createTextNode,
|
|
8
|
+
$getRoot,
|
|
9
|
+
type LexicalEditor,
|
|
10
|
+
type SerializedEditorState,
|
|
11
|
+
} from 'lexical'
|
|
12
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
14
|
+
|
|
15
|
+
import { builtInExtensions } from '../config/built-in-extension-names'
|
|
16
|
+
import { defaultClientEditorConfig } from '../config/default-extensions'
|
|
17
|
+
import { EditorComponent } from '../editor-component'
|
|
18
|
+
import { installFrameControl } from '../test-support/frame-control'
|
|
19
|
+
|
|
20
|
+
// biome-ignore lint/suspicious/noExplicitAny: React act environment flag
|
|
21
|
+
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
|
22
|
+
|
|
23
|
+
defineAdminConfig({
|
|
24
|
+
i18n: {
|
|
25
|
+
admin: { defaultLocale: 'en', locales: ['en'] },
|
|
26
|
+
content: { defaultLocale: 'en', locales: ['en'] },
|
|
27
|
+
},
|
|
28
|
+
collections: [
|
|
29
|
+
{
|
|
30
|
+
path: 'pages',
|
|
31
|
+
labels: { singular: 'Page', plural: 'Pages' },
|
|
32
|
+
fields: [{ name: 'title', label: 'Title', type: 'text' }],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Deterministic frames and idle callbacks.
|
|
39
|
+
*
|
|
40
|
+
* jsdom implements no `requestIdleCallback`, so `editor-component` takes
|
|
41
|
+
* its synchronous branch and an emission scheduled before a value swap
|
|
42
|
+
* can never land after it — hiding an entire class of ordering bug. And
|
|
43
|
+
* the normalization baseline settles after a microtask and two frames,
|
|
44
|
+
* which a fixed delay can only guess at.
|
|
45
|
+
*/
|
|
46
|
+
let frames: ReturnType<typeof installFrameControl>
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
frames = installFrameControl()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
frames.restore()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const text = (value: string) => ({
|
|
57
|
+
detail: 0,
|
|
58
|
+
format: 0,
|
|
59
|
+
mode: 'normal',
|
|
60
|
+
style: '',
|
|
61
|
+
text: value,
|
|
62
|
+
type: 'text',
|
|
63
|
+
version: 1,
|
|
64
|
+
})
|
|
65
|
+
// biome-ignore lint/suspicious/noExplicitAny: serialized fixtures are structural
|
|
66
|
+
const doc = (...children: unknown[]): any => ({
|
|
67
|
+
root: { children, direction: null, format: '', indent: 0, type: 'root', version: 1 },
|
|
68
|
+
})
|
|
69
|
+
const headingNode = (value: string) => ({
|
|
70
|
+
children: [text(value)],
|
|
71
|
+
direction: null,
|
|
72
|
+
format: '',
|
|
73
|
+
indent: 0,
|
|
74
|
+
type: 'heading',
|
|
75
|
+
version: 1,
|
|
76
|
+
tag: 'h1',
|
|
77
|
+
})
|
|
78
|
+
const paragraph = (value: string) => ({
|
|
79
|
+
children: [text(value)],
|
|
80
|
+
direction: null,
|
|
81
|
+
format: '',
|
|
82
|
+
indent: 0,
|
|
83
|
+
type: 'paragraph',
|
|
84
|
+
version: 1,
|
|
85
|
+
})
|
|
86
|
+
const imageDoc = () =>
|
|
87
|
+
doc({
|
|
88
|
+
children: [
|
|
89
|
+
{
|
|
90
|
+
type: 'inline-image',
|
|
91
|
+
version: 1,
|
|
92
|
+
targetDocumentId: 'doc-1',
|
|
93
|
+
targetCollectionPath: 'media',
|
|
94
|
+
src: '/cat.png',
|
|
95
|
+
position: 'full',
|
|
96
|
+
altText: 'a cat',
|
|
97
|
+
width: 10,
|
|
98
|
+
height: 10,
|
|
99
|
+
showCaption: false,
|
|
100
|
+
caption: {
|
|
101
|
+
editorState: {
|
|
102
|
+
root: {
|
|
103
|
+
children: [],
|
|
104
|
+
direction: null,
|
|
105
|
+
format: '',
|
|
106
|
+
indent: 0,
|
|
107
|
+
type: 'root',
|
|
108
|
+
version: 1,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
direction: null,
|
|
115
|
+
format: '',
|
|
116
|
+
indent: 0,
|
|
117
|
+
type: 'paragraph',
|
|
118
|
+
version: 1,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const mounted: Array<{ root: Root; container: HTMLDivElement }> = []
|
|
122
|
+
|
|
123
|
+
afterEach(async () => {
|
|
124
|
+
for (const { root, container } of mounted.splice(0)) {
|
|
125
|
+
await act(async () => {
|
|
126
|
+
root.unmount()
|
|
127
|
+
})
|
|
128
|
+
container.remove()
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
/** Mount EditorComponent and report every value it emits. */
|
|
133
|
+
async function mountField(
|
|
134
|
+
removals: string[],
|
|
135
|
+
value: SerializedEditorState,
|
|
136
|
+
onChange: (next: SerializedEditorState) => void
|
|
137
|
+
): Promise<{ editor: () => LexicalEditor; container: HTMLDivElement }> {
|
|
138
|
+
const extensions = defaultClientEditorConfig.extensions?.clone()
|
|
139
|
+
for (const name of removals) extensions.remove(name)
|
|
140
|
+
|
|
141
|
+
let editor: LexicalEditor | undefined
|
|
142
|
+
function Capture(): null {
|
|
143
|
+
const [instance] = useLexicalComposerContext()
|
|
144
|
+
editor = instance
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const container = document.createElement('div')
|
|
149
|
+
document.body.appendChild(container)
|
|
150
|
+
const root = createRoot(container)
|
|
151
|
+
mounted.push({ root, container })
|
|
152
|
+
|
|
153
|
+
await act(async () => {
|
|
154
|
+
root.render(
|
|
155
|
+
<EditorComponent
|
|
156
|
+
id="field"
|
|
157
|
+
name="field"
|
|
158
|
+
editorConfig={{ ...defaultClientEditorConfig, extensions }}
|
|
159
|
+
value={value}
|
|
160
|
+
onChange={onChange}
|
|
161
|
+
featureChildren={[<Capture key="capture" />]}
|
|
162
|
+
/>
|
|
163
|
+
)
|
|
164
|
+
})
|
|
165
|
+
return {
|
|
166
|
+
container,
|
|
167
|
+
editor: () => {
|
|
168
|
+
if (editor == null) throw new Error('editor never captured')
|
|
169
|
+
return editor
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* These assert on what the editor EMITS, not on what a form ends up
|
|
176
|
+
* holding. From the form's side a suppressed emission and an emission
|
|
177
|
+
* that happens to equal the stored value are indistinguishable — both
|
|
178
|
+
* produce no patch — yet only one is the behaviour the spec asks for.
|
|
179
|
+
*/
|
|
180
|
+
describe('adapted content and the change gate', () => {
|
|
181
|
+
it('emits nothing when adapted content is merely loaded', async () => {
|
|
182
|
+
const onChange = vi.fn()
|
|
183
|
+
await mountField([builtInExtensions.Heading], doc(headingNode('Legacy title')), onChange)
|
|
184
|
+
await act(async () => {
|
|
185
|
+
await frames.settleBaseline()
|
|
186
|
+
})
|
|
187
|
+
await act(async () => {
|
|
188
|
+
frames.flushIdle()
|
|
189
|
+
})
|
|
190
|
+
// The adaptation is presentational. Emitting here would make every
|
|
191
|
+
// affected document dirty on open and persist the adapted form on the
|
|
192
|
+
// next unrelated save.
|
|
193
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('emits the adapted content, including the new text, once edited', async () => {
|
|
197
|
+
const onChange = vi.fn()
|
|
198
|
+
const harness = await mountField(
|
|
199
|
+
[builtInExtensions.Heading],
|
|
200
|
+
doc(headingNode('Legacy title')),
|
|
201
|
+
onChange
|
|
202
|
+
)
|
|
203
|
+
await act(async () => {
|
|
204
|
+
await frames.settleBaseline()
|
|
205
|
+
})
|
|
206
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
harness.editor().update(
|
|
210
|
+
() => {
|
|
211
|
+
$getRoot().selectEnd()
|
|
212
|
+
$getRoot().append($createParagraphNode().append($createTextNode('and more')))
|
|
213
|
+
},
|
|
214
|
+
{ discrete: true }
|
|
215
|
+
)
|
|
216
|
+
})
|
|
217
|
+
await act(async () => {
|
|
218
|
+
frames.flushIdle()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
expect(onChange).toHaveBeenCalled()
|
|
222
|
+
const emitted = JSON.stringify(onChange.mock.calls.at(-1)?.[0])
|
|
223
|
+
// Expected loss: the adaptation persists once the reader edits. What
|
|
224
|
+
// must NOT happen is losing either the original text or the edit.
|
|
225
|
+
expect(emitted).not.toContain('"type":"heading"')
|
|
226
|
+
expect(emitted).toContain('Legacy title')
|
|
227
|
+
expect(emitted).toContain('and more')
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('a refused field must not emit the previous document as its value', () => {
|
|
232
|
+
it('suppresses a deferred emission scheduled before the refusal', async () => {
|
|
233
|
+
const onChange = vi.fn()
|
|
234
|
+
const extensions = defaultClientEditorConfig.extensions?.clone()
|
|
235
|
+
extensions.remove(builtInExtensions.InlineImage)
|
|
236
|
+
const editorConfig = { ...defaultClientEditorConfig, extensions }
|
|
237
|
+
|
|
238
|
+
let editor: LexicalEditor | undefined
|
|
239
|
+
function Capture(): null {
|
|
240
|
+
const [instance] = useLexicalComposerContext()
|
|
241
|
+
editor = instance
|
|
242
|
+
return null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const container = document.createElement('div')
|
|
246
|
+
document.body.appendChild(container)
|
|
247
|
+
const root = createRoot(container)
|
|
248
|
+
mounted.push({ root, container })
|
|
249
|
+
|
|
250
|
+
const paint = async (value: SerializedEditorState) => {
|
|
251
|
+
await act(async () => {
|
|
252
|
+
root.render(
|
|
253
|
+
<EditorComponent
|
|
254
|
+
id="gate"
|
|
255
|
+
name="gate"
|
|
256
|
+
editorConfig={editorConfig}
|
|
257
|
+
value={value}
|
|
258
|
+
onChange={onChange}
|
|
259
|
+
featureChildren={[<Capture key="capture" />]}
|
|
260
|
+
/>
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await paint(doc(paragraph('Original')))
|
|
266
|
+
expect(editor).toBeDefined()
|
|
267
|
+
|
|
268
|
+
// The reader edits. handleChange defers the emission.
|
|
269
|
+
await act(async () => {
|
|
270
|
+
editor?.update(
|
|
271
|
+
() => {
|
|
272
|
+
$getRoot()
|
|
273
|
+
.clear()
|
|
274
|
+
.append($createParagraphNode().append($createTextNode('SECRET EDIT')))
|
|
275
|
+
},
|
|
276
|
+
{ discrete: true }
|
|
277
|
+
)
|
|
278
|
+
})
|
|
279
|
+
// An emission is now scheduled but has not run — the window this
|
|
280
|
+
// test exists to exercise.
|
|
281
|
+
expect(frames.pendingIdle()).toBeGreaterThan(0)
|
|
282
|
+
|
|
283
|
+
// Before it fires, the value swaps to a document this field cannot
|
|
284
|
+
// accept — a locale switch or a version restore.
|
|
285
|
+
await paint(imageDoc())
|
|
286
|
+
expect(container.querySelector('.byline-richtext-notice--refused')).not.toBeNull()
|
|
287
|
+
|
|
288
|
+
// The deferred callback now fires.
|
|
289
|
+
await act(async () => {
|
|
290
|
+
frames.flushIdle()
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// It must NOT emit: the editor does not hold the refused document, so
|
|
294
|
+
// anything it emits would overwrite the image document's body with
|
|
295
|
+
// the previous document's content — exactly the loss refusal exists
|
|
296
|
+
// to prevent.
|
|
297
|
+
const emitted = onChange.mock.calls.map((call) => JSON.stringify(call[0])).join('')
|
|
298
|
+
expect(emitted).not.toContain('SECRET EDIT')
|
|
299
|
+
expect(emitted).not.toContain('Original')
|
|
300
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
301
|
+
})
|
|
302
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) Infonomic Company Limited
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type * as React from 'react'
|
|
12
|
+
import { createContext, useContext, useMemo, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
export type NormalizationStatus =
|
|
15
|
+
| { kind: 'ok' }
|
|
16
|
+
| { kind: 'adapted'; convertedTypes: string[] }
|
|
17
|
+
| { kind: 'refused'; unsupportedTypes: string[] }
|
|
18
|
+
|
|
19
|
+
interface ContextValue {
|
|
20
|
+
status: NormalizationStatus
|
|
21
|
+
setStatus: (status: NormalizationStatus) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const Context = createContext<ContextValue>({
|
|
25
|
+
status: { kind: 'ok' },
|
|
26
|
+
setStatus: () => {},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Carries how the stored value fared against this field's capabilities.
|
|
31
|
+
*
|
|
32
|
+
* Provided ABOVE the composer, like the other shared contexts, so the
|
|
33
|
+
* surface that renders the notice sits outside the editor while the
|
|
34
|
+
* plugin that discovers the status sits inside it.
|
|
35
|
+
*/
|
|
36
|
+
export function NormalizationStatusProvider({
|
|
37
|
+
children,
|
|
38
|
+
}: {
|
|
39
|
+
children: React.ReactNode
|
|
40
|
+
}): React.JSX.Element {
|
|
41
|
+
const [status, setStatus] = useState<NormalizationStatus>({ kind: 'ok' })
|
|
42
|
+
const value = useMemo(() => ({ status, setStatus }), [status])
|
|
43
|
+
return <Context.Provider value={value}>{children}</Context.Provider>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useNormalizationStatus(): ContextValue {
|
|
47
|
+
return useContext(Context)
|
|
48
|
+
}
|