@nextblock-cms/editor 0.15.9 → 0.16.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.
@@ -1,17 +1,55 @@
1
1
  import { default as React } from 'react';
2
2
  import { JSONContent } from '@tiptap/react';
3
- import { Extensions } from '@tiptap/core';
3
+ import { Editor, Extensions } from '@tiptap/core';
4
4
  import { OpenImagePicker } from './utils/mediaPicker';
5
- interface NotionEditorProps {
5
+ export interface NotionEditorProps {
6
6
  content?: string | JSONContent;
7
7
  initialContent?: string | JSONContent;
8
8
  onChange?: (content: string) => void;
9
+ /**
10
+ * Hands the live Tiptap instance to the host application.
11
+ *
12
+ * The editor is otherwise completely sealed: `useEditor` owns the instance,
13
+ * nothing forwards a ref, and there is no context provider, so a sibling of
14
+ * this component has historically had exactly one way to reach the editor —
15
+ * the `window.__nextblockEditor` global assigned further down this file. That
16
+ * global is a genuine race as soon as two editors are mounted at once (a
17
+ * block editor modal opened over a page that already renders one), because
18
+ * the last mount silently wins and the loser's unmount can delete the
19
+ * winner's entry. Anything that needs *this* editor rather than *an* editor
20
+ * should take it from here instead; the global stays in place because other
21
+ * code still reads it.
22
+ *
23
+ * Called with the instance once `useEditor` has produced one, and with `null`
24
+ * on unmount so the host can drop its reference rather than hold a detached
25
+ * ProseMirror view alive. The callback is read through a ref internally, so
26
+ * an inline arrow function is safe to pass: a new identity on every parent
27
+ * render will not re-fire the effect and produce a null/instance flapping
28
+ * sequence.
29
+ */
30
+ onEditorReady?: (editor: Editor | null) => void;
9
31
  onUpdate?: (content: JSONContent) => void;
10
32
  placeholder?: string;
11
33
  editable?: boolean;
12
34
  showToolbar?: boolean;
13
35
  showAiPrompt?: boolean;
14
36
  showCharacterCount?: boolean;
37
+ /**
38
+ * Optional companion UI rendered beside the writing surface — the CMS uses it
39
+ * for the live SEO audit, which has to sit next to the prose it is grading
40
+ * rather than in a dialog the author has to leave the document to open.
41
+ *
42
+ * This library cannot import anything application-specific (it declares no
43
+ * dependencies, ships as a client bundle, and must not reach into
44
+ * `apps/nextblock`), so the panel is injected as a node rather than
45
+ * configured through a flag — the same idiom as `openImagePicker`.
46
+ *
47
+ * When omitted the component renders exactly the tree it always has: no extra
48
+ * wrapper element, no changed class list on the root, and therefore no layout
49
+ * shift for the callers that never pass it. The two-column layout only comes
50
+ * into existence when there is something to put in the second column.
51
+ */
52
+ sidePanel?: React.ReactNode;
15
53
  className?: string;
16
54
  onFocus?: () => void;
17
55
  onBlur?: () => void;
@@ -0,0 +1,25 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import { PluginKey } from '@tiptap/pm/state';
3
+ import { DecorationSet } from '@tiptap/pm/view';
4
+ import { Rgba } from '../../../../utils/src/index.ts';
5
+ export interface LowContrastTextHintOptions {
6
+ /** Set to `false` to disable the hint entirely without unregistering it. */
7
+ enabled: boolean;
8
+ /**
9
+ * Documents larger than this (in ProseMirror content units) skip the walk.
10
+ * The editor's own `CharacterCount` caps authored content at 50,000
11
+ * characters, so this only ever fires for pathological pasted markup, where
12
+ * freezing the tab would be far worse than losing the hint.
13
+ */
14
+ maxDocumentSize: number;
15
+ /** Minimum contrast ratio for body copy. Large text is scaled from this. */
16
+ threshold: number;
17
+ }
18
+ export declare const lowContrastTextHintKey: PluginKey<LowContrastPluginState>;
19
+ interface LowContrastPluginState {
20
+ decorations: DecorationSet;
21
+ /** The resolved editing surface the current `decorations` were measured against. */
22
+ surface: Rgba | null;
23
+ }
24
+ export declare const LowContrastTextHint: Extension<LowContrastTextHintOptions, any>;
25
+ export default LowContrastTextHint;
@@ -0,0 +1,147 @@
1
+ import { Rgba } from '../../../../utils/src/index.ts';
2
+ /** WCAG 2.1 AA minimum contrast for body copy. */
3
+ export declare const BODY_TEXT_CONTRAST_THRESHOLD = 4.5;
4
+ /** WCAG 2.1 AA minimum contrast for large text. */
5
+ export declare const LARGE_TEXT_CONTRAST_THRESHOLD = 3;
6
+ /** WCAG's "large text" floor for regular weights: 18pt === 24px. */
7
+ export declare const LARGE_TEXT_MIN_PX = 24;
8
+ /** WCAG's "large text" floor for bold weights: 14pt === 18.66px. */
9
+ export declare const LARGE_TEXT_BOLD_MIN_PX = 18.66;
10
+ /**
11
+ * Heading levels we are willing to treat as large text when the document does
12
+ * not carry an explicit font size.
13
+ *
14
+ * This is a deliberate approximation and we would rather say so than pretend
15
+ * otherwise: the only way to know a run's *rendered* size is to measure it in
16
+ * the DOM, which would force a layout for every coloured run on every rebuild.
17
+ * So an explicit `font-size` on the run wins when it exists, and otherwise we
18
+ * assume h1-h3 clear 24px in the editor's typography while h4-h6 do not and are
19
+ * held to the stricter body threshold.
20
+ */
21
+ export declare const LARGE_TEXT_MAX_HEADING_LEVEL = 3;
22
+ /** Stable class name on the hint decoration, so integrators can restyle it. */
23
+ export declare const LOW_CONTRAST_HINT_CLASS = "nb-low-contrast-hint";
24
+ export interface LowContrastVerdict {
25
+ flag: boolean;
26
+ ratio: number;
27
+ }
28
+ export interface LargeTextInput {
29
+ bold: boolean;
30
+ fontSize: string | null;
31
+ headingLevel: number | null;
32
+ }
33
+ export interface LowContrastHintAttrsInput {
34
+ backdrop: string;
35
+ foreground: string;
36
+ ratio: number;
37
+ threshold: number;
38
+ }
39
+ /**
40
+ * The index signature is what makes this structurally assignable to
41
+ * ProseMirror's `DecorationAttrs`, which is an open bag of DOM attributes.
42
+ */
43
+ export interface LowContrastHintAttrs {
44
+ [attribute: string]: string;
45
+ 'aria-label': string;
46
+ class: string;
47
+ style: string;
48
+ title: string;
49
+ }
50
+ /**
51
+ * The contrast bar a run has to clear.
52
+ *
53
+ * WCAG does not define a formula linking the body and large-text minimums — it
54
+ * just publishes the pairs (4.5, 3) for AA and (7, 4.5) for AAA. Rather than
55
+ * hardcode a table we scale a caller-supplied body threshold by the AA ratio,
56
+ * which reproduces 3 exactly for the default 4.5 and stays proportional if an
57
+ * integrator raises the bar.
58
+ */
59
+ export declare function contrastThresholdFor(isLargeText: boolean, bodyThreshold?: number): number;
60
+ /**
61
+ * Decide whether a run of text is unreadable against the editing surface.
62
+ *
63
+ * `background` is expected to be opaque because the caller has already
64
+ * flattened the DOM's background stack; any alpha on it is ignored rather than
65
+ * guessed at. A colour we cannot parse is never flagged — a false positive on a
66
+ * value we do not understand would put a black chip behind perfectly readable
67
+ * text — and this function never throws, because it runs inside a ProseMirror
68
+ * transaction handler where an exception would break editing entirely.
69
+ */
70
+ export declare function shouldFlagLowContrast(foreground: string, background: string, isLargeText: boolean, bodyThreshold?: number): LowContrastVerdict;
71
+ /**
72
+ * Flatten a translucent colour onto the surface it is painted over.
73
+ *
74
+ * Contrast is a property of what the eye actually receives, so a 10%-opacity
75
+ * black on white is effectively a pale grey and needs a *dark* chip, not the
76
+ * light one its raw RGB would suggest.
77
+ */
78
+ export declare function flattenColorOver(color: Rgba, background: Rgba): Rgba;
79
+ /**
80
+ * Pick the chip colour to paint behind an unreadable run.
81
+ *
82
+ * Contrast is symmetric, so the colour that would be readable *on* this text is
83
+ * also the colour this text is readable *on*: `readableTextColor` therefore
84
+ * doubles as a backdrop picker, giving pale text a black chip and dark text a
85
+ * white one. Pass the flattened colour from {@link flattenColorOver} — the raw
86
+ * value would mis-classify translucent text.
87
+ *
88
+ * The chip is fully opaque on purpose. Softening it with alpha would blend it
89
+ * back towards the surface and re-introduce exactly the contrast loss we are
90
+ * fixing; the rounded shape and dashed outline are what mark it as an
91
+ * affordance, not transparency.
92
+ */
93
+ export declare function pickBackdropColor(foreground: Rgba): string;
94
+ /**
95
+ * Flatten a stack of background layers, nearest-to-the-text first, onto a
96
+ * fallback surface.
97
+ *
98
+ * The DOM walk collects `background-color` from the text's element and its
99
+ * ancestors; the first opaque layer terminates the stack and everything nearer
100
+ * is composited over it. When nothing opaque is found the caller's fallback
101
+ * stands in.
102
+ */
103
+ export declare function flattenBackgroundLayers(layers: readonly Rgba[], fallback: Rgba): Rgba;
104
+ /**
105
+ * Last-resort guess at the editing surface when every ancestor is transparent
106
+ * all the way up (which happens inside portals and some print stylesheets).
107
+ *
108
+ * Hardcoding white here would break the dark and 'vibrant' CMS themes, where
109
+ * the *dark* author colours are the invisible ones. The default text colour is
110
+ * a reliable proxy for the theme: light default text means a dark page.
111
+ */
112
+ export declare function inferSurfaceFromTextColor(textColor: Rgba): Rgba;
113
+ /**
114
+ * Pull one declaration out of an inline `style` attribute.
115
+ *
116
+ * The editor's `PreserveAllAttributesExtension` and `SpanNode` keep raw `style`
117
+ * strings on nodes and on the `textStyle` mark, so pasted markup can carry a
118
+ * colour that never reaches a typed Tiptap attribute. Later declarations win,
119
+ * matching the cascade. This is a pragmatic split rather than a real CSS parser:
120
+ * a value containing a semicolon (a `url(data:...;base64,...)` background, say)
121
+ * will not be extracted, which for our purposes means "no colour found" and is
122
+ * safely ignored.
123
+ */
124
+ export declare function extractStyleDeclaration(style: string | null | undefined, property: string): string | null;
125
+ /**
126
+ * Convert a CSS font size to pixels.
127
+ *
128
+ * `rem` and `em` are resolved against a 16px root, which is the browser default
129
+ * and what this workspace's Tailwind config assumes; we accept that this is an
130
+ * approximation rather than measuring the cascade, because it only ever moves a
131
+ * run between the 4.5:1 and 3:1 bars.
132
+ */
133
+ export declare function parseFontSizePx(value: string | null | undefined): number | null;
134
+ /** Whether a run qualifies for WCAG's relaxed large-text threshold. */
135
+ export declare function isLargeTextRun(input: LargeTextInput): boolean;
136
+ /** Trim a contrast ratio to the shortest exact decimal, for display. */
137
+ export declare function formatContrastRatio(ratio: number): string;
138
+ /**
139
+ * Build the DOM attributes for a hint decoration.
140
+ *
141
+ * The chip deliberately keeps the author's real colour on top: we are making
142
+ * their text legible, not overriding their choice, and an author who cannot see
143
+ * the text also cannot guess why it suddenly looks different — hence the
144
+ * `title`/`aria-label` quoting the measured ratio and saying, in as many words,
145
+ * that this is an editing aid and is not saved.
146
+ */
147
+ export declare function buildLowContrastHintAttrs(input: LowContrastHintAttrsInput): LowContrastHintAttrs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextblock-cms/editor",
3
- "version": "0.15.9",
3
+ "version": "0.16.1",
4
4
  "main": "index.js",
5
5
  "module": "index.mjs",
6
6
  "types": "index.d.ts",