@astryxdesign/lab 0.1.9-canary.fe6baeb → 0.1.9-canary.fef7dca
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/RichTextEditor/RichTextEditor.d.ts +23 -2
- package/dist/RichTextEditor/RichTextEditor.d.ts.map +1 -1
- package/dist/RichTextEditor/RichTextEditor.js +9 -2
- package/dist/RichTextEditor/editorTheme.d.ts.map +1 -1
- package/dist/RichTextEditor/editorTheme.js +33 -7
- package/dist/RichTextEditor/index.d.ts +1 -1
- package/dist/RichTextEditor/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lab.css +10 -0
- package/package.json +2 -2
- package/src/RichTextEditor/RichTextEditor.doc.mjs +8 -1
- package/src/RichTextEditor/RichTextEditor.test.tsx +228 -8
- package/src/RichTextEditor/RichTextEditor.tsx +31 -4
- package/src/RichTextEditor/editorTheme.ts +63 -3
- package/src/RichTextEditor/index.ts +1 -0
- package/src/index.ts +1 -0
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
import { type ReactNode } from 'react';
|
|
23
23
|
import type { BaseProps } from '@astryxdesign/core';
|
|
24
24
|
import type { SizeValue } from '@astryxdesign/core/utils';
|
|
25
|
+
import { type Transformer } from '@lexical/markdown';
|
|
26
|
+
export type { Transformer } from '@lexical/markdown';
|
|
25
27
|
import type { EditorState, Klass, LexicalEditor, LexicalNode } from 'lexical';
|
|
26
28
|
export type RichTextEditorStatusType = 'warning' | 'error' | 'success';
|
|
27
29
|
export type RichTextEditorSize = 'sm' | 'md' | 'lg';
|
|
@@ -106,10 +108,29 @@ export interface RichTextEditorProps extends Omit<BaseProps, 'onChange' | 'defau
|
|
|
106
108
|
plugins?: ReactNode;
|
|
107
109
|
/**
|
|
108
110
|
* Whether to enable Markdown shortcut typing (e.g. `# ` for a heading,
|
|
109
|
-
* `- ` for a list). Uses the
|
|
111
|
+
* `- ` for a list). Uses the `transformers` prop (defaults to the standard
|
|
112
|
+
* `@lexical/markdown` transformers).
|
|
110
113
|
* @default true
|
|
111
114
|
*/
|
|
112
115
|
hasMarkdownShortcuts?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Markdown transformers — the single source of truth for markdown behaviour.
|
|
118
|
+
* Defaults to the standard `@lexical/markdown` `TRANSFORMERS`.
|
|
119
|
+
*
|
|
120
|
+
* The same array drives all three markdown operations in Lexical (see the
|
|
121
|
+
* lexical-playground reference, where one `PLAYGROUND_TRANSFORMERS` array
|
|
122
|
+
* feeds each):
|
|
123
|
+
* - shortcut typing — `registerMarkdownShortcuts` (wired here today)
|
|
124
|
+
* - markdown -> state — `$convertFromMarkdownString` (future import API)
|
|
125
|
+
* - state -> markdown — `$convertToMarkdownString` (future `getMarkdown`)
|
|
126
|
+
*
|
|
127
|
+
* Pass a custom array to support additional node types (e.g. custom
|
|
128
|
+
* transformers layered in via the `nodes` extension point) consistently
|
|
129
|
+
* across all three. Shortcut typing is only applied when
|
|
130
|
+
* `hasMarkdownShortcuts` is true; the array is still the intended input for
|
|
131
|
+
* the serialization APIs added in later phases.
|
|
132
|
+
*/
|
|
133
|
+
transformers?: ReadonlyArray<Transformer>;
|
|
113
134
|
/** Whether to automatically focus the editor on mount. @default false */
|
|
114
135
|
hasAutoFocus?: boolean;
|
|
115
136
|
/**
|
|
@@ -138,7 +159,7 @@ export interface RichTextEditorProps extends Omit<BaseProps, 'onChange' | 'defau
|
|
|
138
159
|
* />
|
|
139
160
|
* ```
|
|
140
161
|
*/
|
|
141
|
-
export declare function RichTextEditor({ label, isLabelHidden, description, isOptional, isRequired, defaultValue, onChange, placeholder, isReadOnly, isDisabled, status, width, labelTooltip, size: sizeProp, nodes, plugins, hasMarkdownShortcuts, hasAutoFocus, namespace, xstyle, className, style, ...rest }: RichTextEditorProps): import("react").JSX.Element;
|
|
162
|
+
export declare function RichTextEditor({ label, isLabelHidden, description, isOptional, isRequired, defaultValue, onChange, placeholder, isReadOnly, isDisabled, status, width, labelTooltip, size: sizeProp, nodes, plugins, hasMarkdownShortcuts, transformers, hasAutoFocus, namespace, xstyle, className, style, ...rest }: RichTextEditorProps): import("react").JSX.Element;
|
|
142
163
|
export declare namespace RichTextEditor {
|
|
143
164
|
var displayName: string;
|
|
144
165
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/RichTextEditor.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/RichTextEditor.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAoC,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAgBxE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAkBxD,OAAO,EAAe,KAAK,WAAW,EAAC,MAAM,mBAAmB,CAAC;AACjE,YAAY,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAKnD,OAAO,KAAK,EACV,WAAW,EACX,KAAK,EACL,aAAa,EACb,WAAW,EAEZ,MAAM,SAAS,CAAC;AAiEjB,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAEvE,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEpD,MAAM,WAAW,oBAAoB;IACnC,qCAAqC;IACrC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,mBAAoB,SAAQ,IAAI,CAC/C,SAAS,EACT,UAAU,GAAG,cAAc,CAC5B;IACC,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IACrE,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,IAAI,CAAC,EAAE,kBAAkB,CAAC;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC1C;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,YAAY,CAAC,EAAE,aAAa,CAAC,WAAW,CAAC,CAAC;IAC1C,yEAAyE;IACzE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,aAAqB,EACrB,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,YAAY,EACZ,QAAQ,EACR,WAAW,EACX,UAAkB,EAClB,UAAkB,EAClB,MAAM,EACN,KAAK,EACL,YAAY,EACZ,IAAI,EAAE,QAAQ,EACd,KAAK,EACL,OAAO,EACP,oBAA2B,EAC3B,YAA2B,EAC3B,YAAoB,EACpB,SAA2B,EAC3B,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,mBAAmB,+BAwHrB;yBAjJe,cAAc"}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* Component Specification Protocol. `lexical` and `@lexical/*` are OPTIONAL peer
|
|
24
24
|
* dependencies — install them to use this component.
|
|
25
25
|
*/
|
|
26
|
-
import { useEffect, useId, useRef } from 'react';
|
|
26
|
+
import { useEffect, useId, useMemo, useRef } from 'react';
|
|
27
27
|
import * as stylex from '@stylexjs/stylex';
|
|
28
28
|
import '@astryxdesign/core/theme/tokens.stylex';
|
|
29
29
|
import { colorVars, spacingVars, typographyVars, typeScaleVars } from '@astryxdesign/core/theme/tokens.stylex';
|
|
@@ -114,6 +114,7 @@ export function RichTextEditor({
|
|
|
114
114
|
nodes,
|
|
115
115
|
plugins,
|
|
116
116
|
hasMarkdownShortcuts = true,
|
|
117
|
+
transformers = TRANSFORMERS,
|
|
117
118
|
hasAutoFocus = false,
|
|
118
119
|
namespace = 'astryx-editor',
|
|
119
120
|
xstyle,
|
|
@@ -133,6 +134,12 @@ export function RichTextEditor({
|
|
|
133
134
|
themeRef.current = sharedEditorTheme();
|
|
134
135
|
}
|
|
135
136
|
const editable = !isReadOnly && !isDisabled;
|
|
137
|
+
|
|
138
|
+
// Stabilize the transformers array so MarkdownShortcutPlugin doesn't
|
|
139
|
+
// re-register on every render. `[...transformers]` would allocate a new
|
|
140
|
+
// array each time; memoize on the prop identity instead. (React Compiler
|
|
141
|
+
// isn't running the transform in this repo, so this isn't auto-memoized.)
|
|
142
|
+
const markdownTransformers = useMemo(() => [...transformers], [transformers]);
|
|
136
143
|
const initialConfig = {
|
|
137
144
|
namespace,
|
|
138
145
|
theme: themeRef.current,
|
|
@@ -189,7 +196,7 @@ export function RichTextEditor({
|
|
|
189
196
|
placeholder: null,
|
|
190
197
|
ErrorBoundary: LexicalErrorBoundary
|
|
191
198
|
}), /*#__PURE__*/_jsx(HistoryPlugin, {}), /*#__PURE__*/_jsx(ListPlugin, {}), /*#__PURE__*/_jsx(LinkPlugin, {}), /*#__PURE__*/_jsx(TabIndentationPlugin, {}), hasMarkdownShortcuts && /*#__PURE__*/_jsx(MarkdownShortcutPlugin, {
|
|
192
|
-
transformers:
|
|
199
|
+
transformers: markdownTransformers
|
|
193
200
|
}), hasAutoFocus && /*#__PURE__*/_jsx(AutoFocusOnMount, {}), onChange && /*#__PURE__*/_jsx(OnChangePlugin, {
|
|
194
201
|
onChange: onChange,
|
|
195
202
|
ignoreHistoryMergeTagChange: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/editorTheme.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/editorTheme.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,SAAS,CAAC;AA2GhD;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,kBAAkB,CA8CtD"}
|
|
@@ -19,6 +19,24 @@ import { colorVars, spacingVars, radiusVars, typographyVars, typeScaleVars, font
|
|
|
19
19
|
* class-name strings, which `stylex.props(...).className` yields.
|
|
20
20
|
*/
|
|
21
21
|
export function sharedEditorTheme() {
|
|
22
|
+
const ulClass = {
|
|
23
|
+
className: "xsq74q5 x31w388 xtaz4m5 x43c9pm"
|
|
24
|
+
}.className ?? '';
|
|
25
|
+
const olClass = {
|
|
26
|
+
className: "xsq74q5 x31w388 x3yw8vx x43c9pm"
|
|
27
|
+
}.className ?? '';
|
|
28
|
+
const ulDepth2Class = {
|
|
29
|
+
className: "x31w388 x43c9pm x10im51j xrylv2j"
|
|
30
|
+
}.className ?? '';
|
|
31
|
+
const ulDepth3Class = {
|
|
32
|
+
className: "x31w388 x43c9pm x10im51j x152237o"
|
|
33
|
+
}.className ?? '';
|
|
34
|
+
const olDepth2Class = {
|
|
35
|
+
className: "x31w388 x43c9pm x10im51j x1rrqm6h"
|
|
36
|
+
}.className ?? '';
|
|
37
|
+
const olDepth3Class = {
|
|
38
|
+
className: "x31w388 x43c9pm x10im51j x19k9dhg"
|
|
39
|
+
}.className ?? '';
|
|
22
40
|
return {
|
|
23
41
|
paragraph: {
|
|
24
42
|
className: "xsq74q5"
|
|
@@ -38,15 +56,23 @@ export function sharedEditorTheme() {
|
|
|
38
56
|
className: "x1g06x3t xrxpjvj x126nfab xct1zlm x1t7ytsu x14ydlgx xv1l7n4"
|
|
39
57
|
}.className,
|
|
40
58
|
list: {
|
|
41
|
-
ul:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
className: "xsq74q5 x31w388"
|
|
59
|
+
ul: ulClass,
|
|
60
|
+
ol: olClass,
|
|
61
|
+
checklist: {
|
|
62
|
+
className: "xsq74q5 x43c9pm x3ct3a4 x12gdq22"
|
|
46
63
|
}.className,
|
|
47
64
|
listitem: {
|
|
48
|
-
className: "x1yqqeh1"
|
|
49
|
-
}.className
|
|
65
|
+
className: "x1yqqeh1 x1sle589"
|
|
66
|
+
}.className,
|
|
67
|
+
nested: {
|
|
68
|
+
listitem: {
|
|
69
|
+
className: "x1yqqeh1 x1sle589"
|
|
70
|
+
}.className
|
|
71
|
+
},
|
|
72
|
+
// Depth arrays cycle via `depth % length`, so three entries give three
|
|
73
|
+
// distinct nesting levels before repeating (disc→circle→square, etc.).
|
|
74
|
+
ulDepth: [ulClass, ulDepth2Class, ulDepth3Class],
|
|
75
|
+
olDepth: [olClass, olDepth2Class, olDepth3Class]
|
|
50
76
|
},
|
|
51
77
|
link: {
|
|
52
78
|
className: "xjse4m1 x1bvjpef x1ypdohk"
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* SYNC: When modified, update this header and the editor component doc files.
|
|
9
9
|
*/
|
|
10
10
|
export { RichTextEditor } from './RichTextEditor';
|
|
11
|
-
export type { RichTextEditorProps, RichTextEditorStatus, RichTextEditorStatusType, RichTextEditorSize, } from './RichTextEditor';
|
|
11
|
+
export type { RichTextEditorProps, RichTextEditorStatus, RichTextEditorStatusType, RichTextEditorSize, Transformer, } from './RichTextEditor';
|
|
12
12
|
export { RichTextView } from './RichTextView';
|
|
13
13
|
export type { RichTextViewProps } from './RichTextView';
|
|
14
14
|
export { sharedEditorTheme } from './editorTheme';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAChD,YAAY,EACV,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/RichTextEditor/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AAEH,OAAO,EAAC,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAChD,YAAY,EACV,mBAAmB,EACnB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAC5C,YAAY,EAAC,iBAAiB,EAAC,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,5 +27,5 @@ export { SankeyChart, type SankeyChartProps, SankeyLink, type SankeyLinkProps, S
|
|
|
27
27
|
export * from './Stepper';
|
|
28
28
|
export * from './CircularProgress';
|
|
29
29
|
export { LogStream, type LogStreamProps, type LogEntry, type LogStreamLevel, } from './LogStream';
|
|
30
|
-
export { RichTextEditor, type RichTextEditorProps, type RichTextEditorStatus, type RichTextEditorStatusType, type RichTextEditorSize, RichTextView, type RichTextViewProps, sharedEditorTheme, } from './RichTextEditor';
|
|
30
|
+
export { RichTextEditor, type RichTextEditorProps, type RichTextEditorStatus, type RichTextEditorStatusType, type RichTextEditorSize, type Transformer, RichTextView, type RichTextViewProps, sharedEditorTheme, } from './RichTextEditor';
|
|
31
31
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/lab.css
CHANGED
|
@@ -111,6 +111,7 @@
|
|
|
111
111
|
.xz51j44:not(#\#):not(#\#){grid-row:3}
|
|
112
112
|
.x33j1x8:not(#\#):not(#\#){grid-row:var(--x-gridRow)}
|
|
113
113
|
.x17y0mx6:not(#\#):not(#\#){inset-inline:0}
|
|
114
|
+
.x10im51j:not(#\#):not(#\#){margin-block:0}
|
|
114
115
|
.x1yqqeh1:not(#\#):not(#\#){margin-block:var(--spacing-0-5)}
|
|
115
116
|
.xsq74q5:not(#\#):not(#\#){margin-block:var(--spacing-1)}
|
|
116
117
|
.x1g06x3t:not(#\#):not(#\#){margin-block:var(--spacing-2)}
|
|
@@ -370,7 +371,15 @@
|
|
|
370
371
|
.x1cpk1wn:not(#\#):not(#\#):not(#\#){line-height:var(--text-heading-2-leading)}
|
|
371
372
|
.x1kq96og:not(#\#):not(#\#):not(#\#){line-height:var(--text-label-leading)}
|
|
372
373
|
.x1ltkj2j:not(#\#):not(#\#):not(#\#){line-height:var(--text-supporting-leading)}
|
|
374
|
+
.x43c9pm:not(#\#):not(#\#):not(#\#){list-style-position:outside}
|
|
375
|
+
.xrylv2j:not(#\#):not(#\#):not(#\#){list-style-type:circle}
|
|
376
|
+
.x3yw8vx:not(#\#):not(#\#):not(#\#){list-style-type:decimal}
|
|
377
|
+
.xtaz4m5:not(#\#):not(#\#):not(#\#){list-style-type:disc}
|
|
378
|
+
.x1sle589:not(#\#):not(#\#):not(#\#){list-style-type:inherit}
|
|
379
|
+
.x1rrqm6h:not(#\#):not(#\#):not(#\#){list-style-type:lower-alpha}
|
|
380
|
+
.x19k9dhg:not(#\#):not(#\#):not(#\#){list-style-type:lower-roman}
|
|
373
381
|
.x3ct3a4:not(#\#):not(#\#):not(#\#){list-style-type:none}
|
|
382
|
+
.x152237o:not(#\#):not(#\#):not(#\#){list-style-type:square}
|
|
374
383
|
.x1h1dc9q:not(#\#):not(#\#):not(#\#){margin-inline-end:calc(var(--spacing-0-5) + var(--border-width))}
|
|
375
384
|
.xhi58n4:not(#\#):not(#\#):not(#\#){margin-inline-start:calc(var(--spacing-1) * -1)}
|
|
376
385
|
.xladpz9:not(#\#):not(#\#):not(#\#){margin-inline-start:var(--spacing-0-5)}
|
|
@@ -389,6 +398,7 @@
|
|
|
389
398
|
.xmarwi5:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--icon-inline-offset)}
|
|
390
399
|
.x18gyask:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--spacing-0)}
|
|
391
400
|
.x1vsv5vr:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--spacing-1)}
|
|
401
|
+
.x12gdq22:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--spacing-2)}
|
|
392
402
|
.x126nfab:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--spacing-3)}
|
|
393
403
|
.x1rey3nv:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--spacing-4)}
|
|
394
404
|
.x31w388:not(#\#):not(#\#):not(#\#){padding-inline-start:var(--spacing-6)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/lab",
|
|
3
|
-
"version": "0.1.9-canary.
|
|
3
|
+
"version": "0.1.9-canary.fef7dca",
|
|
4
4
|
"description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
|
|
5
5
|
"author": "Meta Open Source",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@stylexjs/stylex": ">=0.10.0",
|
|
48
|
-
"@astryxdesign/core": "0.1.9-canary.
|
|
48
|
+
"@astryxdesign/core": "0.1.9-canary.fef7dca",
|
|
49
49
|
"react": ">=19.0.0",
|
|
50
50
|
"react-dom": ">=19.0.0",
|
|
51
51
|
"lexical": "^0.46.0",
|
|
@@ -93,9 +93,16 @@ export const docs = {
|
|
|
93
93
|
name: 'hasMarkdownShortcuts',
|
|
94
94
|
type: 'boolean',
|
|
95
95
|
description:
|
|
96
|
-
'Enable Markdown shortcut typing (e.g. "# " for a heading). Uses
|
|
96
|
+
'Enable Markdown shortcut typing (e.g. "# " for a heading). Uses the transformers prop (defaults to the standard @lexical/markdown transformers).',
|
|
97
97
|
default: 'true',
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
name: 'transformers',
|
|
101
|
+
type: 'ReadonlyArray<Transformer>',
|
|
102
|
+
description:
|
|
103
|
+
'Markdown transformers — the single source of truth for markdown behaviour. Defaults to the standard @lexical/markdown TRANSFORMERS. In Lexical the same array drives all three markdown operations (shortcut typing, markdown->state import, state->markdown export); this prop wires shortcut typing today and is the intended input for the serialization APIs added in later phases. Pass a custom array to support additional node types (e.g. transformers layered in via the nodes extension point) consistently across all three. Shortcut typing is only applied when hasMarkdownShortcuts is true.',
|
|
104
|
+
default: 'TRANSFORMERS',
|
|
105
|
+
},
|
|
99
106
|
{
|
|
100
107
|
name: 'hasAutoFocus',
|
|
101
108
|
type: 'boolean',
|
|
@@ -16,11 +16,132 @@ import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
|
|
16
16
|
import type {LexicalEditor} from 'lexical';
|
|
17
17
|
import {$getRoot, $createParagraphNode, $createTextNode} from 'lexical';
|
|
18
18
|
import {HeadingNode} from '@lexical/rich-text';
|
|
19
|
+
import {
|
|
20
|
+
TRANSFORMERS,
|
|
21
|
+
$convertFromMarkdownString,
|
|
22
|
+
} from '@lexical/markdown';
|
|
19
23
|
import {RichTextEditor} from './RichTextEditor';
|
|
20
24
|
import {RichTextView} from './RichTextView';
|
|
21
25
|
|
|
26
|
+
// Small plugin that captures the editor instance so tests can drive real
|
|
27
|
+
// Lexical updates (jsdom does not implement contenteditable editing).
|
|
28
|
+
function CaptureEditor({
|
|
29
|
+
onReady,
|
|
30
|
+
}: {
|
|
31
|
+
onReady: (editor: LexicalEditor) => void;
|
|
32
|
+
}) {
|
|
33
|
+
const [editor] = useLexicalComposerContext();
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
onReady(editor);
|
|
36
|
+
}, [editor, onReady]);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
// A minimal valid serialized Lexical editor state containing a single
|
|
23
41
|
// paragraph with the text "Hello world".
|
|
42
|
+
// Builds a serialized Lexical state containing a list. `listType` is
|
|
43
|
+
// 'bullet' (renders <ul>) or 'number' (renders <ol>). Used to verify that the
|
|
44
|
+
// editor theme applies visible list markers rather than bare indentation.
|
|
45
|
+
function makeListState(listType: 'bullet' | 'number'): string {
|
|
46
|
+
const tag = listType === 'number' ? 'ol' : 'ul';
|
|
47
|
+
return JSON.stringify({
|
|
48
|
+
root: {
|
|
49
|
+
children: [
|
|
50
|
+
{
|
|
51
|
+
children: [
|
|
52
|
+
{
|
|
53
|
+
children: [
|
|
54
|
+
{
|
|
55
|
+
detail: 0,
|
|
56
|
+
format: 0,
|
|
57
|
+
mode: 'normal',
|
|
58
|
+
style: '',
|
|
59
|
+
text: 'Item one',
|
|
60
|
+
type: 'text',
|
|
61
|
+
version: 1,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
direction: 'ltr',
|
|
65
|
+
format: '',
|
|
66
|
+
indent: 0,
|
|
67
|
+
type: 'listitem',
|
|
68
|
+
version: 1,
|
|
69
|
+
value: 1,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
direction: 'ltr',
|
|
73
|
+
format: '',
|
|
74
|
+
indent: 0,
|
|
75
|
+
type: 'list',
|
|
76
|
+
version: 1,
|
|
77
|
+
listType,
|
|
78
|
+
start: 1,
|
|
79
|
+
tag,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
direction: 'ltr',
|
|
83
|
+
format: '',
|
|
84
|
+
indent: 0,
|
|
85
|
+
type: 'root',
|
|
86
|
+
version: 1,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Builds a serialized Lexical state with a two-level nested bullet list:
|
|
92
|
+
// • Item one
|
|
93
|
+
// ◦ Nested item
|
|
94
|
+
// Lexical models nesting as a child `list` node inside a `listitem`. Used to
|
|
95
|
+
// verify that depth-2 markers differ from depth-1 (disc → circle) rather than
|
|
96
|
+
// falling back to bare indentation.
|
|
97
|
+
function makeNestedBulletState(): string {
|
|
98
|
+
const textNode = (text: string) => ({
|
|
99
|
+
detail: 0,
|
|
100
|
+
format: 0,
|
|
101
|
+
mode: 'normal',
|
|
102
|
+
style: '',
|
|
103
|
+
text,
|
|
104
|
+
type: 'text',
|
|
105
|
+
version: 1,
|
|
106
|
+
});
|
|
107
|
+
const listItem = (children: unknown[], value: number) => ({
|
|
108
|
+
children,
|
|
109
|
+
direction: 'ltr',
|
|
110
|
+
format: '',
|
|
111
|
+
indent: 0,
|
|
112
|
+
type: 'listitem',
|
|
113
|
+
version: 1,
|
|
114
|
+
value,
|
|
115
|
+
});
|
|
116
|
+
const bulletList = (children: unknown[]) => ({
|
|
117
|
+
children,
|
|
118
|
+
direction: 'ltr',
|
|
119
|
+
format: '',
|
|
120
|
+
indent: 0,
|
|
121
|
+
type: 'list',
|
|
122
|
+
version: 1,
|
|
123
|
+
listType: 'bullet',
|
|
124
|
+
start: 1,
|
|
125
|
+
tag: 'ul',
|
|
126
|
+
});
|
|
127
|
+
return JSON.stringify({
|
|
128
|
+
root: {
|
|
129
|
+
children: [
|
|
130
|
+
bulletList([
|
|
131
|
+
listItem([textNode('Item one')], 1),
|
|
132
|
+
// A nested list lives inside its own list item wrapper.
|
|
133
|
+
listItem([bulletList([listItem([textNode('Nested item')], 1)])], 2),
|
|
134
|
+
]),
|
|
135
|
+
],
|
|
136
|
+
direction: 'ltr',
|
|
137
|
+
format: '',
|
|
138
|
+
indent: 0,
|
|
139
|
+
type: 'root',
|
|
140
|
+
version: 1,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
24
145
|
const HELLO_STATE = JSON.stringify({
|
|
25
146
|
root: {
|
|
26
147
|
children: [
|
|
@@ -112,19 +233,12 @@ describe('RichTextEditor', () => {
|
|
|
112
233
|
// instance via a small capture plugin and drive a real Lexical update,
|
|
113
234
|
// then assert onChange fires.
|
|
114
235
|
let editorRef: LexicalEditor | undefined;
|
|
115
|
-
function CaptureEditor() {
|
|
116
|
-
const [editor] = useLexicalComposerContext();
|
|
117
|
-
useEffect(() => {
|
|
118
|
-
editorRef = editor;
|
|
119
|
-
}, [editor]);
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
236
|
const onChange = vi.fn();
|
|
123
237
|
render(
|
|
124
238
|
<RichTextEditor
|
|
125
239
|
label="Notes"
|
|
126
240
|
onChange={onChange}
|
|
127
|
-
plugins={<CaptureEditor />}
|
|
241
|
+
plugins={<CaptureEditor onReady={e => (editorRef = e)} />}
|
|
128
242
|
/>,
|
|
129
243
|
);
|
|
130
244
|
await waitFor(() => expect(editorRef).toBeDefined());
|
|
@@ -155,6 +269,70 @@ describe('RichTextEditor', () => {
|
|
|
155
269
|
);
|
|
156
270
|
expect(screen.getByTestId('custom-plugin')).toBeInTheDocument();
|
|
157
271
|
});
|
|
272
|
+
|
|
273
|
+
it('accepts a custom transformers array without throwing', () => {
|
|
274
|
+
// Empty transformer set is a valid custom configuration (disables all
|
|
275
|
+
// markdown shortcuts while keeping the plugin mounted).
|
|
276
|
+
render(<RichTextEditor label="Notes" transformers={[]} />);
|
|
277
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('renders when markdown shortcuts are disabled', () => {
|
|
281
|
+
render(<RichTextEditor label="Notes" hasMarkdownShortcuts={false} />);
|
|
282
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('applies the default transformers to convert markdown to a heading', async () => {
|
|
286
|
+
// jsdom can't dispatch the keystrokes that trigger registerMarkdownShortcuts
|
|
287
|
+
// live, so we drive $convertFromMarkdownString with the same TRANSFORMERS
|
|
288
|
+
// the component registers by default. This proves the default transformer
|
|
289
|
+
// set actually produces the expected node structure (a heading), rather
|
|
290
|
+
// than only asserting the editor mounts.
|
|
291
|
+
let editorRef: LexicalEditor | undefined;
|
|
292
|
+
render(
|
|
293
|
+
<RichTextEditor
|
|
294
|
+
label="Notes"
|
|
295
|
+
plugins={<CaptureEditor onReady={e => (editorRef = e)} />}
|
|
296
|
+
/>,
|
|
297
|
+
);
|
|
298
|
+
await waitFor(() => expect(editorRef).toBeDefined());
|
|
299
|
+
editorRef!.update(() => {
|
|
300
|
+
$convertFromMarkdownString('# Title', TRANSFORMERS);
|
|
301
|
+
});
|
|
302
|
+
await waitFor(() => {
|
|
303
|
+
editorRef!.getEditorState().read(() => {
|
|
304
|
+
const first = $getRoot().getFirstChild();
|
|
305
|
+
expect(first?.getType()).toBe('heading');
|
|
306
|
+
expect(first?.getTextContent()).toBe('Title');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('leaves markdown untransformed when given an empty transformers array', async () => {
|
|
312
|
+
// With no transformers, the same markdown text stays a plain paragraph —
|
|
313
|
+
// demonstrating the transformers prop is the effective source of truth for
|
|
314
|
+
// markdown behaviour, not a fixed internal default.
|
|
315
|
+
let editorRef: LexicalEditor | undefined;
|
|
316
|
+
render(
|
|
317
|
+
<RichTextEditor
|
|
318
|
+
label="Notes"
|
|
319
|
+
transformers={[]}
|
|
320
|
+
plugins={<CaptureEditor onReady={e => (editorRef = e)} />}
|
|
321
|
+
/>,
|
|
322
|
+
);
|
|
323
|
+
await waitFor(() => expect(editorRef).toBeDefined());
|
|
324
|
+
editorRef!.update(() => {
|
|
325
|
+
// Empty transformer set: markdown syntax is preserved verbatim.
|
|
326
|
+
$convertFromMarkdownString('# Title', []);
|
|
327
|
+
});
|
|
328
|
+
await waitFor(() => {
|
|
329
|
+
editorRef!.getEditorState().read(() => {
|
|
330
|
+
const first = $getRoot().getFirstChild();
|
|
331
|
+
expect(first?.getType()).toBe('paragraph');
|
|
332
|
+
expect(first?.getTextContent()).toBe('# Title');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
158
336
|
});
|
|
159
337
|
|
|
160
338
|
describe('RichTextView', () => {
|
|
@@ -213,6 +391,48 @@ describe('RichTextView', () => {
|
|
|
213
391
|
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
|
214
392
|
});
|
|
215
393
|
|
|
394
|
+
it('renders bullet lists with a disc marker (not bare indentation)', async () => {
|
|
395
|
+
const {container} = render(<RichTextView value={makeListState('bullet')} />);
|
|
396
|
+
await waitFor(() =>
|
|
397
|
+
expect(screen.getByText('Item one')).toBeInTheDocument(),
|
|
398
|
+
);
|
|
399
|
+
const ul = container.querySelector('ul');
|
|
400
|
+
expect(ul).not.toBeNull();
|
|
401
|
+
// The theme must give the <ul> a marker class so the browser draws a
|
|
402
|
+
// bullet. A bare list with only padding would show indentation only —
|
|
403
|
+
// the exact bug this guards against.
|
|
404
|
+
expect(ul?.className.trim()).not.toBe('');
|
|
405
|
+
expect(getComputedStyle(ul as Element).listStyleType).toBe('disc');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('renders numbered lists with a decimal marker (not bare indentation)', async () => {
|
|
409
|
+
const {container} = render(<RichTextView value={makeListState('number')} />);
|
|
410
|
+
await waitFor(() =>
|
|
411
|
+
expect(screen.getByText('Item one')).toBeInTheDocument(),
|
|
412
|
+
);
|
|
413
|
+
const ol = container.querySelector('ol');
|
|
414
|
+
expect(ol).not.toBeNull();
|
|
415
|
+
expect(ol?.className.trim()).not.toBe('');
|
|
416
|
+
expect(getComputedStyle(ol as Element).listStyleType).toBe('decimal');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('renders nested bullet lists with a distinct depth-2 marker (circle)', async () => {
|
|
420
|
+
const {container} = render(
|
|
421
|
+
<RichTextView value={makeNestedBulletState()} />,
|
|
422
|
+
);
|
|
423
|
+
await waitFor(() =>
|
|
424
|
+
expect(screen.getByText('Nested item')).toBeInTheDocument(),
|
|
425
|
+
);
|
|
426
|
+
const lists = container.querySelectorAll('ul');
|
|
427
|
+
// Outer <ul> plus the nested <ul>.
|
|
428
|
+
expect(lists.length).toBe(2);
|
|
429
|
+
const [outer, nested] = lists;
|
|
430
|
+
expect(getComputedStyle(outer).listStyleType).toBe('disc');
|
|
431
|
+
// Depth-2 must cycle to a different marker so nesting is visually legible,
|
|
432
|
+
// matching the native browser disc → circle progression.
|
|
433
|
+
expect(getComputedStyle(nested).listStyleType).toBe('circle');
|
|
434
|
+
});
|
|
435
|
+
|
|
216
436
|
it('renders nothing (no crash) on malformed JSON with no fallback', () => {
|
|
217
437
|
expect(() => render(<RichTextView value={'garbage'} />)).not.toThrow();
|
|
218
438
|
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* dependencies — install them to use this component.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import {useEffect, useId, useRef, type ReactNode} from 'react';
|
|
27
|
+
import {useEffect, useId, useMemo, useRef, type ReactNode} from 'react';
|
|
28
28
|
import * as stylex from '@stylexjs/stylex';
|
|
29
29
|
import {
|
|
30
30
|
colorVars,
|
|
@@ -59,7 +59,8 @@ import {LinkPlugin} from '@lexical/react/LexicalLinkPlugin';
|
|
|
59
59
|
import {TabIndentationPlugin} from '@lexical/react/LexicalTabIndentationPlugin';
|
|
60
60
|
import {MarkdownShortcutPlugin} from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
|
61
61
|
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
|
|
62
|
-
import {TRANSFORMERS} from '@lexical/markdown';
|
|
62
|
+
import {TRANSFORMERS, type Transformer} from '@lexical/markdown';
|
|
63
|
+
export type {Transformer} from '@lexical/markdown';
|
|
63
64
|
import {ListNode, ListItemNode} from '@lexical/list';
|
|
64
65
|
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
|
|
65
66
|
import {LinkNode, AutoLinkNode} from '@lexical/link';
|
|
@@ -224,10 +225,29 @@ export interface RichTextEditorProps extends Omit<
|
|
|
224
225
|
plugins?: ReactNode;
|
|
225
226
|
/**
|
|
226
227
|
* Whether to enable Markdown shortcut typing (e.g. `# ` for a heading,
|
|
227
|
-
* `- ` for a list). Uses the
|
|
228
|
+
* `- ` for a list). Uses the `transformers` prop (defaults to the standard
|
|
229
|
+
* `@lexical/markdown` transformers).
|
|
228
230
|
* @default true
|
|
229
231
|
*/
|
|
230
232
|
hasMarkdownShortcuts?: boolean;
|
|
233
|
+
/**
|
|
234
|
+
* Markdown transformers — the single source of truth for markdown behaviour.
|
|
235
|
+
* Defaults to the standard `@lexical/markdown` `TRANSFORMERS`.
|
|
236
|
+
*
|
|
237
|
+
* The same array drives all three markdown operations in Lexical (see the
|
|
238
|
+
* lexical-playground reference, where one `PLAYGROUND_TRANSFORMERS` array
|
|
239
|
+
* feeds each):
|
|
240
|
+
* - shortcut typing — `registerMarkdownShortcuts` (wired here today)
|
|
241
|
+
* - markdown -> state — `$convertFromMarkdownString` (future import API)
|
|
242
|
+
* - state -> markdown — `$convertToMarkdownString` (future `getMarkdown`)
|
|
243
|
+
*
|
|
244
|
+
* Pass a custom array to support additional node types (e.g. custom
|
|
245
|
+
* transformers layered in via the `nodes` extension point) consistently
|
|
246
|
+
* across all three. Shortcut typing is only applied when
|
|
247
|
+
* `hasMarkdownShortcuts` is true; the array is still the intended input for
|
|
248
|
+
* the serialization APIs added in later phases.
|
|
249
|
+
*/
|
|
250
|
+
transformers?: ReadonlyArray<Transformer>;
|
|
231
251
|
/** Whether to automatically focus the editor on mount. @default false */
|
|
232
252
|
hasAutoFocus?: boolean;
|
|
233
253
|
/**
|
|
@@ -275,6 +295,7 @@ export function RichTextEditor({
|
|
|
275
295
|
nodes,
|
|
276
296
|
plugins,
|
|
277
297
|
hasMarkdownShortcuts = true,
|
|
298
|
+
transformers = TRANSFORMERS,
|
|
278
299
|
hasAutoFocus = false,
|
|
279
300
|
namespace = 'astryx-editor',
|
|
280
301
|
xstyle,
|
|
@@ -296,6 +317,12 @@ export function RichTextEditor({
|
|
|
296
317
|
|
|
297
318
|
const editable = !isReadOnly && !isDisabled;
|
|
298
319
|
|
|
320
|
+
// Stabilize the transformers array so MarkdownShortcutPlugin doesn't
|
|
321
|
+
// re-register on every render. `[...transformers]` would allocate a new
|
|
322
|
+
// array each time; memoize on the prop identity instead. (React Compiler
|
|
323
|
+
// isn't running the transform in this repo, so this isn't auto-memoized.)
|
|
324
|
+
const markdownTransformers = useMemo(() => [...transformers], [transformers]);
|
|
325
|
+
|
|
299
326
|
const initialConfig: InitialConfigType = {
|
|
300
327
|
namespace,
|
|
301
328
|
theme: themeRef.current,
|
|
@@ -379,7 +406,7 @@ export function RichTextEditor({
|
|
|
379
406
|
<LinkPlugin />
|
|
380
407
|
<TabIndentationPlugin />
|
|
381
408
|
{hasMarkdownShortcuts && (
|
|
382
|
-
<MarkdownShortcutPlugin transformers={
|
|
409
|
+
<MarkdownShortcutPlugin transformers={markdownTransformers} />
|
|
383
410
|
)}
|
|
384
411
|
{hasAutoFocus && <AutoFocusOnMount />}
|
|
385
412
|
{onChange && (
|
|
@@ -53,12 +53,50 @@ const editorTheme = stylex.create({
|
|
|
53
53
|
borderInlineStartColor: colorVars['--color-border-emphasized'],
|
|
54
54
|
color: colorVars['--color-text-secondary'],
|
|
55
55
|
},
|
|
56
|
-
|
|
56
|
+
ul: {
|
|
57
57
|
marginBlock: spacingVars['--spacing-1'],
|
|
58
58
|
paddingInlineStart: spacingVars['--spacing-6'],
|
|
59
|
+
listStyleType: 'disc',
|
|
60
|
+
listStylePosition: 'outside',
|
|
61
|
+
},
|
|
62
|
+
ol: {
|
|
63
|
+
marginBlock: spacingVars['--spacing-1'],
|
|
64
|
+
paddingInlineStart: spacingVars['--spacing-6'],
|
|
65
|
+
listStyleType: 'decimal',
|
|
66
|
+
listStylePosition: 'outside',
|
|
67
|
+
},
|
|
68
|
+
// Nested lists: no extra vertical margin, and cycle marker styles per depth
|
|
69
|
+
// to match native browser list nesting. Lexical indexes ulDepth/olDepth by
|
|
70
|
+
// `depth % array.length`, so three entries give three distinct levels that
|
|
71
|
+
// then repeat — matching the browser default disc → circle → square cycle.
|
|
72
|
+
ulNested: {
|
|
73
|
+
marginBlock: 0,
|
|
74
|
+
},
|
|
75
|
+
ulDepth2: {
|
|
76
|
+
listStyleType: 'circle',
|
|
77
|
+
},
|
|
78
|
+
ulDepth3: {
|
|
79
|
+
listStyleType: 'square',
|
|
80
|
+
},
|
|
81
|
+
olNested: {
|
|
82
|
+
marginBlock: 0,
|
|
83
|
+
},
|
|
84
|
+
olDepth2: {
|
|
85
|
+
listStyleType: 'lower-alpha',
|
|
86
|
+
},
|
|
87
|
+
olDepth3: {
|
|
88
|
+
listStyleType: 'lower-roman',
|
|
89
|
+
},
|
|
90
|
+
// Checklists render as a <ul listtype="check">; Lexical draws checkbox
|
|
91
|
+
// affordances on the list items, so suppress the disc marker here.
|
|
92
|
+
checklist: {
|
|
93
|
+
listStyleType: 'none',
|
|
94
|
+
paddingInlineStart: spacingVars['--spacing-2'],
|
|
59
95
|
},
|
|
60
96
|
listItem: {
|
|
61
97
|
marginBlock: spacingVars['--spacing-0-5'],
|
|
98
|
+
// Ensure the marker is shown (some CSS resets set list-style: none on li).
|
|
99
|
+
listStyleType: 'inherit',
|
|
62
100
|
},
|
|
63
101
|
link: {
|
|
64
102
|
color: colorVars['--color-text-accent'],
|
|
@@ -94,6 +132,20 @@ const editorTheme = stylex.create({
|
|
|
94
132
|
* class-name strings, which `stylex.props(...).className` yields.
|
|
95
133
|
*/
|
|
96
134
|
export function sharedEditorTheme(): EditorThemeClasses {
|
|
135
|
+
const ulClass = stylex.props(editorTheme.ul).className ?? '';
|
|
136
|
+
const olClass = stylex.props(editorTheme.ol).className ?? '';
|
|
137
|
+
const ulDepth2Class =
|
|
138
|
+
stylex.props(editorTheme.ul, editorTheme.ulNested, editorTheme.ulDepth2)
|
|
139
|
+
.className ?? '';
|
|
140
|
+
const ulDepth3Class =
|
|
141
|
+
stylex.props(editorTheme.ul, editorTheme.ulNested, editorTheme.ulDepth3)
|
|
142
|
+
.className ?? '';
|
|
143
|
+
const olDepth2Class =
|
|
144
|
+
stylex.props(editorTheme.ol, editorTheme.olNested, editorTheme.olDepth2)
|
|
145
|
+
.className ?? '';
|
|
146
|
+
const olDepth3Class =
|
|
147
|
+
stylex.props(editorTheme.ol, editorTheme.olNested, editorTheme.olDepth3)
|
|
148
|
+
.className ?? '';
|
|
97
149
|
return {
|
|
98
150
|
paragraph: stylex.props(editorTheme.paragraph).className,
|
|
99
151
|
heading: {
|
|
@@ -103,9 +155,17 @@ export function sharedEditorTheme(): EditorThemeClasses {
|
|
|
103
155
|
},
|
|
104
156
|
quote: stylex.props(editorTheme.quote).className,
|
|
105
157
|
list: {
|
|
106
|
-
ul:
|
|
107
|
-
ol:
|
|
158
|
+
ul: ulClass,
|
|
159
|
+
ol: olClass,
|
|
160
|
+
checklist: stylex.props(editorTheme.ul, editorTheme.checklist).className,
|
|
108
161
|
listitem: stylex.props(editorTheme.listItem).className,
|
|
162
|
+
nested: {
|
|
163
|
+
listitem: stylex.props(editorTheme.listItem).className,
|
|
164
|
+
},
|
|
165
|
+
// Depth arrays cycle via `depth % length`, so three entries give three
|
|
166
|
+
// distinct nesting levels before repeating (disc→circle→square, etc.).
|
|
167
|
+
ulDepth: [ulClass, ulDepth2Class, ulDepth3Class],
|
|
168
|
+
olDepth: [olClass, olDepth2Class, olDepth3Class],
|
|
109
169
|
},
|
|
110
170
|
link: stylex.props(editorTheme.link).className,
|
|
111
171
|
text: {
|