@astryxdesign/lab 0.1.9-canary.d2d84f8 → 0.1.9-canary.f94bbeb

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.
@@ -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 default `@lexical/markdown` transformers.
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,EAA2B,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAgB/D,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,0BAA0B,CAAC;AAuBxD,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;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,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,YAAoB,EACpB,SAA2B,EAC3B,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,IAAI,EACR,EAAE,mBAAmB,+BAkHrB;yBA1Ie,cAAc"}
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: 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;AAqEhD;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,kBAAkB,CAwBtD"}
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
- className: "xsq74q5 x31w388"
43
- }.className,
44
- ol: {
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,GACnB,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"}
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
@@ -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.d2d84f8",
3
+ "version": "0.1.9-canary.f94bbeb",
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.d2d84f8",
48
+ "@astryxdesign/core": "0.1.9-canary.f94bbeb",
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 default @lexical/markdown transformers.',
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 default `@lexical/markdown` transformers.
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={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
- list: {
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: stylex.props(editorTheme.list).className,
107
- ol: stylex.props(editorTheme.list).className,
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: {
@@ -18,6 +18,7 @@ export type {
18
18
  RichTextEditorStatus,
19
19
  RichTextEditorStatusType,
20
20
  RichTextEditorSize,
21
+ Transformer,
21
22
  } from './RichTextEditor';
22
23
 
23
24
  export {RichTextView} from './RichTextView';
package/src/index.ts CHANGED
@@ -238,6 +238,7 @@ export {
238
238
  type RichTextEditorStatus,
239
239
  type RichTextEditorStatusType,
240
240
  type RichTextEditorSize,
241
+ type Transformer,
241
242
  RichTextView,
242
243
  type RichTextViewProps,
243
244
  sharedEditorTheme,