@fabrika/annotations 0.2.0 → 0.3.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/package.json +9 -1
- package/src/AnnotationEditor.tsx +4 -3
- package/src/AnnotationProvider.tsx +8 -4
- package/src/schemas.ts +39 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/annotations",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React annotation state, text/block/region locators, editor and feedback surface.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"types": "./src/runtime.ts",
|
|
30
30
|
"default": "./src/runtime.ts"
|
|
31
31
|
},
|
|
32
|
+
"./schemas": {
|
|
33
|
+
"bun": "./src/schemas.ts",
|
|
34
|
+
"types": "./src/schemas.ts",
|
|
35
|
+
"default": "./src/schemas.ts"
|
|
36
|
+
},
|
|
32
37
|
"./styles.css": "./src/styles.css",
|
|
33
38
|
"./testing/dom.ts": {
|
|
34
39
|
"bun": "./src/testing/dom.ts",
|
|
@@ -49,5 +54,8 @@
|
|
|
49
54
|
"@happy-dom/global-registrator": {
|
|
50
55
|
"optional": true
|
|
51
56
|
}
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"zod": "^3.25.76"
|
|
52
60
|
}
|
|
53
61
|
}
|
package/src/AnnotationEditor.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AnnotationHost, useAnnotationHost } from '@fabrika/annotations/runtime'
|
|
2
2
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { localUploadResponseSchema, sharedUploadResponseSchema } from './schemas'
|
|
3
4
|
import { autoResizeTextarea, hostAcceptsUploads } from './utils'
|
|
4
5
|
|
|
5
6
|
interface AnnotationEditorProps {
|
|
@@ -44,11 +45,11 @@ async function uploadImage(host: AnnotationHost, file: File): Promise<string | n
|
|
|
44
45
|
formData.append('image', file)
|
|
45
46
|
try {
|
|
46
47
|
const resp = await fetch(endpoint(), { method: 'POST', body: formData })
|
|
47
|
-
const data = await resp.json()
|
|
48
|
+
const data: unknown = await resp.json()
|
|
48
49
|
// Local daemon returns { path }; worker returns { url }. Normalise to
|
|
49
50
|
// a single string that `imageToUrl` can resolve.
|
|
50
|
-
if (host.isShared) return data.url || null
|
|
51
|
-
return data.path || null
|
|
51
|
+
if (host.isShared) return sharedUploadResponseSchema.parse(data).url || null
|
|
52
|
+
return localUploadResponseSchema.parse(data).path || null
|
|
52
53
|
} catch {
|
|
53
54
|
return null
|
|
54
55
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AnnotationCtx, SessionContext, useAnnotationHost } from '@fabrika/annotations/runtime'
|
|
2
|
-
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
3
3
|
import type { Annotation, AnnotationState } from './runtime'
|
|
4
|
+
import { annotationStateSchema } from './schemas'
|
|
4
5
|
import { useAnnotationState } from './useAnnotationState'
|
|
5
6
|
|
|
6
7
|
export { useAnnotations } from '@fabrika/annotations/runtime'
|
|
@@ -24,6 +25,7 @@ function AnnotationDraft({ scope, isReadOnly = false, remoteAnnotations, loadSta
|
|
|
24
25
|
const [annotations, setAnnotations] = useState<Annotation[]>([])
|
|
25
26
|
const [generalNote, setGeneralNote] = useState('')
|
|
26
27
|
const [hydrated, setHydrated] = useState(false)
|
|
28
|
+
const savedState = useRef<AnnotationState | null>(null)
|
|
27
29
|
const value = useAnnotationState({
|
|
28
30
|
annotations,
|
|
29
31
|
setAnnotations,
|
|
@@ -39,8 +41,10 @@ function AnnotationDraft({ scope, isReadOnly = false, remoteAnnotations, loadSta
|
|
|
39
41
|
setHydrated(false)
|
|
40
42
|
loadState(scope).then((saved) => {
|
|
41
43
|
if (cancelled) return
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
const result = annotationStateSchema.safeParse(saved)
|
|
45
|
+
savedState.current = result.success ? result.data : null
|
|
46
|
+
setAnnotations(result.success ? result.data.annotations : [])
|
|
47
|
+
setGeneralNote(result.success ? result.data.generalNote : '')
|
|
44
48
|
setHydrated(true)
|
|
45
49
|
})
|
|
46
50
|
return () => {
|
|
@@ -50,7 +54,7 @@ function AnnotationDraft({ scope, isReadOnly = false, remoteAnnotations, loadSta
|
|
|
50
54
|
|
|
51
55
|
useEffect(() => {
|
|
52
56
|
if (!hydrated || isReadOnly) return
|
|
53
|
-
const timer = setTimeout(() => saveState(scope, { annotations, generalNote }), 300)
|
|
57
|
+
const timer = setTimeout(() => saveState(scope, { ...savedState.current, annotations, generalNote }), 300)
|
|
54
58
|
return () => clearTimeout(timer)
|
|
55
59
|
}, [scope, annotations, generalNote, hydrated, isReadOnly, saveState])
|
|
56
60
|
|
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const annotationContextSchema = z.object({
|
|
4
|
+
before: z.string(),
|
|
5
|
+
after: z.string(),
|
|
6
|
+
hierarchy: z.array(z.string()),
|
|
7
|
+
lineStart: z.number().optional(),
|
|
8
|
+
lineEnd: z.number().optional(),
|
|
9
|
+
}).passthrough()
|
|
10
|
+
|
|
11
|
+
export const annotationAuthorSchema = z.object({ id: z.string(), name: z.string() }).passthrough()
|
|
12
|
+
export const annotationAttachmentSchema = z.object({ url: z.string(), mime: z.string().optional() }).passthrough()
|
|
13
|
+
|
|
14
|
+
export const annotationSchema = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
snippet: z.string(),
|
|
17
|
+
note: z.string(),
|
|
18
|
+
createdAt: z.string(),
|
|
19
|
+
filePath: z.string().optional(),
|
|
20
|
+
canvasFile: z.string().optional(),
|
|
21
|
+
context: annotationContextSchema.optional(),
|
|
22
|
+
images: z.array(z.string()).optional(),
|
|
23
|
+
attachments: z.array(annotationAttachmentSchema).optional(),
|
|
24
|
+
source: z.enum(['local', 'remote']).optional(),
|
|
25
|
+
author: annotationAuthorSchema.optional(),
|
|
26
|
+
}).passthrough()
|
|
27
|
+
|
|
28
|
+
// A damaged entry must not discard the other annotations in a saved draft.
|
|
29
|
+
export const recoveredAnnotationsSchema = z.array(annotationSchema.optional().catch(undefined)).transform(entries =>
|
|
30
|
+
entries.filter(entry => entry !== undefined)
|
|
31
|
+
).catch([])
|
|
32
|
+
|
|
33
|
+
export const annotationStateSchema = z.object({
|
|
34
|
+
annotations: recoveredAnnotationsSchema,
|
|
35
|
+
generalNote: z.string().catch(''),
|
|
36
|
+
}).passthrough()
|
|
37
|
+
|
|
38
|
+
export const localUploadResponseSchema = z.object({ path: z.string() })
|
|
39
|
+
export const sharedUploadResponseSchema = z.object({ url: z.string() })
|