@bikdotai/bik-component-library 0.0.850-beta.2 → 0.0.850-beta.4

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.
Files changed (59) hide show
  1. package/dist/cjs/assets/icons/ShipStation.svg.js +1 -1
  2. package/dist/cjs/assets/icons/ShipStation.svg.js.map +1 -1
  3. package/dist/cjs/components/agent-builder/AgentBuilder.styled.js +0 -1
  4. package/dist/cjs/components/agent-builder/AgentBuilder.styled.js.map +1 -1
  5. package/dist/cjs/components/agent-builder/components/DiscardModal.js +1 -1
  6. package/dist/cjs/components/agent-builder/components/DiscardModal.js.map +1 -1
  7. package/dist/cjs/components/agent-builder/components/GoLiveModal.js +1 -1
  8. package/dist/cjs/components/agent-builder/components/GoLiveModal.js.map +1 -1
  9. package/dist/cjs/components/agent-builder/components/MentionEditor.js +1 -1
  10. package/dist/cjs/components/agent-builder/components/MentionEditor.js.map +1 -1
  11. package/dist/cjs/components/agent-builder/components/MentionPicker.js +32 -24
  12. package/dist/cjs/components/agent-builder/components/MentionPicker.js.map +1 -1
  13. package/dist/cjs/components/agent-listing/AgentListing.js +1 -1
  14. package/dist/cjs/components/agent-listing/AgentListing.js.map +1 -1
  15. package/dist/cjs/components/agent-listing/AiAgentsEmptyState.js +2 -0
  16. package/dist/cjs/components/agent-listing/AiAgentsEmptyState.js.map +1 -0
  17. package/dist/cjs/editor/BikEditor.js +1 -1
  18. package/dist/cjs/editor/BikEditor.js.map +1 -1
  19. package/dist/cjs/editor/BikEditor.styles.js +17 -12
  20. package/dist/cjs/editor/BikEditor.styles.js.map +1 -1
  21. package/dist/cjs/editor/BikEditor.types.js.map +1 -1
  22. package/dist/cjs/editor/extensions/buildExtensions.js +2 -2
  23. package/dist/cjs/editor/extensions/buildExtensions.js.map +1 -1
  24. package/dist/cjs/editor/extensions/characterLimit/CharacterLimitExtension.js +3 -3
  25. package/dist/cjs/editor/extensions/characterLimit/CharacterLimitExtension.js.map +1 -1
  26. package/dist/cjs/index.js +1 -1
  27. package/dist/esm/assets/icons/ShipStation.svg.js +2 -2
  28. package/dist/esm/assets/icons/ShipStation.svg.js.map +1 -1
  29. package/dist/esm/assets/icons/index.d.ts +1 -0
  30. package/dist/esm/components/agent-builder/AgentBuilder.styled.js +4 -5
  31. package/dist/esm/components/agent-builder/AgentBuilder.styled.js.map +1 -1
  32. package/dist/esm/components/agent-builder/components/DiscardModal.js +57 -49
  33. package/dist/esm/components/agent-builder/components/DiscardModal.js.map +1 -1
  34. package/dist/esm/components/agent-builder/components/GoLiveModal.js +26 -22
  35. package/dist/esm/components/agent-builder/components/GoLiveModal.js.map +1 -1
  36. package/dist/esm/components/agent-builder/components/MentionEditor.js +24 -23
  37. package/dist/esm/components/agent-builder/components/MentionEditor.js.map +1 -1
  38. package/dist/esm/components/agent-builder/components/MentionPicker.js +81 -73
  39. package/dist/esm/components/agent-builder/components/MentionPicker.js.map +1 -1
  40. package/dist/esm/components/agent-listing/AgentListing.js +68 -74
  41. package/dist/esm/components/agent-listing/AgentListing.js.map +1 -1
  42. package/dist/esm/components/agent-listing/AiAgentsEmptyState.d.ts +22 -0
  43. package/dist/esm/components/agent-listing/AiAgentsEmptyState.js +22 -0
  44. package/dist/esm/components/agent-listing/AiAgentsEmptyState.js.map +1 -0
  45. package/dist/esm/editor/BikEditor.js +70 -68
  46. package/dist/esm/editor/BikEditor.js.map +1 -1
  47. package/dist/esm/editor/BikEditor.styles.js +20 -15
  48. package/dist/esm/editor/BikEditor.styles.js.map +1 -1
  49. package/dist/esm/editor/BikEditor.types.d.ts +10 -0
  50. package/dist/esm/editor/BikEditor.types.js.map +1 -1
  51. package/dist/esm/editor/extensions/buildExtensions.d.ts +6 -0
  52. package/dist/esm/editor/extensions/buildExtensions.js +20 -15
  53. package/dist/esm/editor/extensions/buildExtensions.js.map +1 -1
  54. package/dist/esm/editor/extensions/characterLimit/CharacterLimitExtension.d.ts +20 -3
  55. package/dist/esm/editor/extensions/characterLimit/CharacterLimitExtension.js +40 -25
  56. package/dist/esm/editor/extensions/characterLimit/CharacterLimitExtension.js.map +1 -1
  57. package/dist/esm/index.js +761 -759
  58. package/dist/esm/index.js.map +1 -1
  59. package/package.json +1 -1
@@ -9,17 +9,17 @@ import E from "../../node_modules/@tiptap/extension-paragraph/dist/index.js";
9
9
  import m from "@tiptap/extension-placeholder";
10
10
  import y from "@tiptap/extension-subscript";
11
11
  import T from "@tiptap/extension-superscript";
12
- import P from "@tiptap/extension-text-align";
13
- import { TextStyle as L } from "@tiptap/extension-text-style";
14
- import D from "@tiptap/extension-underline";
12
+ import L from "@tiptap/extension-text-align";
13
+ import { TextStyle as P } from "@tiptap/extension-text-style";
14
+ import H from "@tiptap/extension-underline";
15
15
  import l from "@tiptap/starter-kit";
16
- import { CharacterLimitExtension as H } from "./characterLimit/CharacterLimitExtension.js";
16
+ import { CharacterLimitExtension as D } from "./characterLimit/CharacterLimitExtension.js";
17
17
  import { FontSizeExtension as I } from "./FontSizeExtension.js";
18
18
  import { JsonHighlightExtension as w } from "./jsonHighlight/JsonHighlightExtension.js";
19
19
  import { buildAgentMentionExtension as B, buildTeamMentionExtension as O } from "./mention/MentionExtension.js";
20
20
  import { PasteExtension as R } from "./paste/PasteExtension.js";
21
- import { PasteNormalizationExtension as g } from "./plainClipboard/PasteNormalizationExtension.js";
22
- import { SectionDividerNode as A } from "./sectionDivider/SectionDividerNode.js";
21
+ import { PasteNormalizationExtension as A } from "./plainClipboard/PasteNormalizationExtension.js";
22
+ import { SectionDividerNode as g } from "./sectionDivider/SectionDividerNode.js";
23
23
  import { SendShortcutExtension as d } from "./sendShortcut/SendShortcutExtension.js";
24
24
  import { buildSlashCommandExtension as z } from "./slashCommand/SlashCommandExtension.js";
25
25
  import { VariableDecorationExtension as F } from "./variable/VariableDecorationExtension.js";
@@ -54,19 +54,19 @@ function le(e) {
54
54
  }),
55
55
  ...e.maxCharacters ? [s.configure({ limit: e.maxCharacters })] : []
56
56
  ];
57
- const o = e.features?.richPaste ?? !1, p = e.features?.richTypography ?? !1, n = e.features?.allowedMarks, t = (r) => !n || n.includes(r), u = k.extend({
57
+ const o = e.features?.richPaste ?? !1, p = e.features?.richTypography ?? !1, a = e.features?.allowedMarks, t = (r) => !a || a.includes(r), u = k.extend({
58
58
  addKeyboardShortcuts() {
59
59
  return {
60
60
  "Shift-Enter": () => this.editor.commands.setHardBreak()
61
61
  };
62
62
  }
63
- }), a = (r) => r === "0" || r === "0px" || r === "0pt" || r === "0em", h = E.extend({
63
+ }), n = (r) => r === "0" || r === "0px" || r === "0pt" || r === "0em", h = E.extend({
64
64
  addAttributes() {
65
65
  return {
66
66
  ...this.parent?.() ?? {},
67
67
  margin: {
68
68
  default: null,
69
- parseHTML: (r) => a(r.style.marginTop) && a(r.style.marginBottom) ? "0" : null,
69
+ parseHTML: (r) => n(r.style.marginTop) && n(r.style.marginBottom) ? "0" : null,
70
70
  renderHTML: (r) => r.margin === "0" ? { style: "margin: 0px" } : {}
71
71
  }
72
72
  };
@@ -87,7 +87,7 @@ function le(e) {
87
87
  }),
88
88
  h,
89
89
  u,
90
- ...t("underline") ? [D] : [],
90
+ ...t("underline") ? [H] : [],
91
91
  // Extend Link to strip addPasteRules() and addInputRules() so pasting a
92
92
  // URL never creates a hyperlink regardless of autolink/linkOnPaste values.
93
93
  C.extend({
@@ -102,7 +102,7 @@ function le(e) {
102
102
  class: "bik-link"
103
103
  }
104
104
  }),
105
- L,
105
+ P,
106
106
  m.configure({
107
107
  placeholder: e.placeholder ?? "Type a message..."
108
108
  }),
@@ -110,7 +110,7 @@ function le(e) {
110
110
  // consumed and neither PlainClipboardExtension nor the editor's default
111
111
  // paste handling will run for that event.
112
112
  ...e.onPaste ? [R.configure({ onPaste: e.onPaste })] : [],
113
- g.configure({
113
+ A.configure({
114
114
  preserveMarks: o,
115
115
  // Only email + signature (richPaste) keep the rich HTML paste pipeline.
116
116
  // Every other channel (WhatsApp, IG, FB, comments) forces plain-text
@@ -124,12 +124,17 @@ function le(e) {
124
124
  }),
125
125
  F,
126
126
  ...e.maxCharacters ? [s.configure({ limit: e.maxCharacters })] : [],
127
- ...e.characterLimit ? [H.configure({ limit: e.characterLimit })] : [],
127
+ ...e.characterLimit ? [
128
+ D.configure({
129
+ limit: e.characterLimit,
130
+ hard: e.characterLimitHard
131
+ })
132
+ ] : [],
128
133
  // Always register SectionDividerNode so the custom <div data-section-divider>
129
134
  // node is part of the schema in every editor instance. This means imperative
130
135
  // callers (setBodyAndSections / setSectionContent) work even when no
131
136
  // `sections` prop was provided at mount time.
132
- A
137
+ g
133
138
  ], f = [
134
139
  ...e.mentions?.agents ? [
135
140
  B(
@@ -163,7 +168,7 @@ function le(e) {
163
168
  M.configure({ multicolor: !0 }),
164
169
  S,
165
170
  I,
166
- P.configure({ types: ["heading", "paragraph"] }),
171
+ L.configure({ types: ["heading", "paragraph"] }),
167
172
  y,
168
173
  T,
169
174
  b
@@ -1 +1 @@
1
- {"version":3,"file":"buildExtensions.js","sources":["../../../../src/editor/extensions/buildExtensions.ts"],"sourcesContent":["import CharacterCount from '@tiptap/extension-character-count';\nimport Color from '@tiptap/extension-color';\nimport FontFamily from '@tiptap/extension-font-family';\nimport HardBreak from '@tiptap/extension-hard-break';\nimport Highlight from '@tiptap/extension-highlight';\nimport Image from '@tiptap/extension-image';\nimport Link from '@tiptap/extension-link';\nimport Paragraph from '@tiptap/extension-paragraph';\nimport Placeholder from '@tiptap/extension-placeholder';\nimport Subscript from '@tiptap/extension-subscript';\nimport Superscript from '@tiptap/extension-superscript';\nimport TextAlign from '@tiptap/extension-text-align';\nimport { TextStyle } from '@tiptap/extension-text-style';\nimport Underline from '@tiptap/extension-underline';\nimport StarterKit from '@tiptap/starter-kit';\nimport type { ReactNode } from 'react';\nimport type {\n\tEditorFeatures,\n\tEditorSnapshot,\n\tKeyboardShortcut,\n\tMentionDropdownRenderProps,\n\tMentionItem,\n\tPasteData,\n\tSlashCommandDropdownRenderProps,\n\tSlashCommandItem,\n} from '../BikEditor.types';\nimport { CharacterLimitExtension } from './characterLimit/CharacterLimitExtension';\nimport { FontSizeExtension } from './FontSizeExtension';\nimport { JsonHighlightExtension } from './jsonHighlight/JsonHighlightExtension';\nimport {\n\tbuildAgentMentionExtension,\n\tbuildTeamMentionExtension,\n} from './mention/MentionExtension';\nimport { PasteExtension } from './paste/PasteExtension';\nimport { PasteNormalizationExtension } from './plainClipboard/PasteNormalizationExtension';\nimport { SectionDividerNode } from './sectionDivider/SectionDividerNode';\nimport { SendShortcutExtension } from './sendShortcut/SendShortcutExtension';\nimport { buildSlashCommandExtension } from './slashCommand/SlashCommandExtension';\nimport { VariableDecorationExtension } from './variable/VariableDecorationExtension';\n\ninterface ExtensionConfig {\n\tfeatures?: EditorFeatures;\n\tplaceholder?: string;\n\tmaxCharacters?: number;\n\t/**\n\t * Soft character limit. Unlike `maxCharacters` (which hard-blocks all input),\n\t * this blocks typing past the limit but ALLOWS paste to overflow, rendering\n\t * the excess characters greyed (`.bik-editor-overflow`). Purely visual +\n\t * typing-guard; the consumer shows the count/error and gates send itself.\n\t */\n\tcharacterLimit?: number;\n\thasSections?: boolean;\n\tmentions?: {\n\t\tagents?: { current: MentionItem[] };\n\t\tteams?: { current: MentionItem[] };\n\t};\n\tslashCommands?: { current: SlashCommandItem[] };\n\tonPaste?: (data: PasteData) => boolean | void;\n\tonSend?: (content: EditorSnapshot) => void;\n\tsendShortcut?:\n\t\t| { key: string; modifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'> }\n\t\t| Array<{\n\t\t\t\tkey: string;\n\t\t\t\tmodifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'>;\n\t\t }>;\n\tshortcuts?: KeyboardShortcut[];\n\tonMentionSelected?: (item: MentionItem, char: '@' | '#') => void;\n\tonSlashCommandSelected?: (command: SlashCommandItem) => void;\n\trenderMentionItem?: (item: MentionItem, isActive: boolean) => ReactNode;\n\trenderMentionDropdown?: (props: MentionDropdownRenderProps) => ReactNode;\n\tremoveMentionTriggerOnDismiss?: boolean;\n\trenderMentionChipIcon?: (\n\t\tsource: string | null,\n\t\tkind: string | null,\n\t) => ReactNode;\n\trenderSlashCommandItem?: (\n\t\titem: SlashCommandItem,\n\t\tisActive: boolean,\n\t) => ReactNode;\n\trenderSlashCommandDropdown?: (\n\t\tprops: SlashCommandDropdownRenderProps,\n\t) => ReactNode;\n}\n\nexport function buildExtensions(config: ExtensionConfig) {\n\t// JSON body mode is a dedicated code editor — it deliberately skips the rich\n\t// text stack (mentions, typography, rich paste, sections) and turns the whole\n\t// document into one syntax-highlighted JSON code block, Postman-style.\n\tif (config.features?.jsonMode) {\n\t\treturn [\n\t\t\tStarterKit.configure({\n\t\t\t\tlink: false,\n\t\t\t\tunderline: false,\n\t\t\t\t// Keep codeBlock (the JSON container) and history; drop everything\n\t\t\t\t// that has no meaning inside raw JSON.\n\t\t\t\tbold: false,\n\t\t\t\titalic: false,\n\t\t\t\tstrike: false,\n\t\t\t\tcode: false,\n\t\t\t\theading: false,\n\t\t\t\tbulletList: false,\n\t\t\t\torderedList: false,\n\t\t\t\tblockquote: false,\n\t\t\t\thorizontalRule: false,\n\t\t\t}),\n\t\t\tPlaceholder.configure({\n\t\t\t\tplaceholder: config.placeholder ?? '{\\n \\n}',\n\t\t\t}),\n\t\t\tJsonHighlightExtension,\n\t\t\tSendShortcutExtension.configure({\n\t\t\t\tonSend: config.onSend,\n\t\t\t\tsendShortcut: config.sendShortcut,\n\t\t\t\textraShortcuts: config.shortcuts ?? [],\n\t\t\t}),\n\t\t\t...(config.maxCharacters\n\t\t\t\t? [CharacterCount.configure({ limit: config.maxCharacters })]\n\t\t\t\t: []),\n\t\t];\n\t}\n\n\tconst hasRichPaste = config.features?.richPaste ?? false;\n\tconst hasRichTypography = config.features?.richTypography ?? false;\n\n\t// When allowedMarks is specified only those marks are registered in the schema.\n\t// This disables rendering, keyboard shortcuts AND paste-preservation for the\n\t// excluded marks — all three come for free when the extension is absent.\n\tconst allowedMarks = config.features?.allowedMarks;\n\tconst isMark = (m: 'bold' | 'italic' | 'strike' | 'underline' | 'code') =>\n\t\t!allowedMarks || allowedMarks.includes(m);\n\n\t// Soft line break. Shift-Enter inserts a <br> inside the current block\n\t// (matches Gmail); plain Enter still splits the block into a new paragraph\n\t// via StarterKit's default. HardBreak is registered standalone — like Link\n\t// and Underline above — instead of through StarterKit, so we can bind ONLY\n\t// Shift-Enter here and leave Mod-Enter free for the send shortcut.\n\tconst softBreak = HardBreak.extend({\n\t\taddKeyboardShortcuts() {\n\t\t\treturn {\n\t\t\t\t'Shift-Enter': () => this.editor.commands.setHardBreak(),\n\t\t\t};\n\t\t},\n\t});\n\n\t// Paragraph that preserves a zero block-margin from the source (Gmail-style).\n\t// Most sources (ChatGPT, typed text) use plain <p> → no margin attr → the\n\t// editor/viewer default 1em margin provides the gap. Google-Docs-style\n\t// content uses margin:0 paragraphs and a separate blank-line paragraph for\n\t// spacing; we keep that margin:0 so the blank line — not stacked margins —\n\t// provides a single-line gap, exactly like Gmail. The paste pipeline also\n\t// sets margin:'0' on every paragraph when a paste contains blank-line\n\t// paragraphs (see applyParagraphSpacing), so spacing comes from EITHER the\n\t// margin OR the blank line, never both (no double gap).\n\tconst isZeroLen = (v: string) =>\n\t\tv === '0' || v === '0px' || v === '0pt' || v === '0em';\n\tconst ParagraphWithMargin = Paragraph.extend({\n\t\taddAttributes() {\n\t\t\treturn {\n\t\t\t\t...(this.parent?.() ?? {}),\n\t\t\t\tmargin: {\n\t\t\t\t\tdefault: null,\n\t\t\t\t\tparseHTML: (el: HTMLElement) =>\n\t\t\t\t\t\tisZeroLen(el.style.marginTop) && isZeroLen(el.style.marginBottom)\n\t\t\t\t\t\t\t? '0'\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\trenderHTML: (attrs: { margin?: string | null }) =>\n\t\t\t\t\t\tattrs.margin === '0' ? { style: 'margin: 0px' } : {},\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n\n\tconst base = [\n\t\t// Exclude Link and Underline from StarterKit — TipTap v3 bundles both.\n\t\t// Without this, two copies of each are registered and the StarterKit copy's\n\t\t// click handler calls window.open even when openOnClick: false is set.\n\t\tStarterKit.configure({\n\t\t\tlink: false,\n\t\t\tunderline: false,\n\t\t\thardBreak: false,\n\t\t\tparagraph: false,\n\t\t\tbold: isMark('bold') ? {} : false,\n\t\t\titalic: isMark('italic') ? {} : false,\n\t\t\tstrike: isMark('strike') ? {} : false,\n\t\t\tcode: isMark('code') ? {} : false,\n\t\t}),\n\t\tParagraphWithMargin,\n\t\tsoftBreak,\n\t\t...(isMark('underline') ? [Underline] : []),\n\t\t// Extend Link to strip addPasteRules() and addInputRules() so pasting a\n\t\t// URL never creates a hyperlink regardless of autolink/linkOnPaste values.\n\t\tLink.extend({\n\t\t\taddPasteRules: () => [],\n\t\t\taddInputRules: () => [],\n\t\t}).configure({\n\t\t\topenOnClick: false,\n\t\t\tautolink: false,\n\t\t\tlinkOnPaste: false,\n\t\t\tHTMLAttributes: {\n\t\t\t\trel: 'noopener noreferrer',\n\t\t\t\tclass: 'bik-link',\n\t\t\t},\n\t\t}),\n\t\tTextStyle,\n\t\tPlaceholder.configure({\n\t\t\tplaceholder: config.placeholder ?? 'Type a message...',\n\t\t}),\n\t\t// Consumer paste callback runs first. If it returns true the event is\n\t\t// consumed and neither PlainClipboardExtension nor the editor's default\n\t\t// paste handling will run for that event.\n\t\t...(config.onPaste\n\t\t\t? [PasteExtension.configure({ onPaste: config.onPaste })]\n\t\t\t: []),\n\t\tPasteNormalizationExtension.configure({\n\t\t\tpreserveMarks: hasRichPaste,\n\t\t\t// Only email + signature (richPaste) keep the rich HTML paste pipeline.\n\t\t\t// Every other channel (WhatsApp, IG, FB, comments) forces plain-text\n\t\t\t// paste for BOTH Cmd/Ctrl+V and Cmd/Ctrl+Shift+V — no formatting.\n\t\t\tplainOnly: !hasRichPaste,\n\t\t}),\n\t\tSendShortcutExtension.configure({\n\t\t\tonSend: config.onSend,\n\t\t\tsendShortcut: config.sendShortcut,\n\t\t\textraShortcuts: config.shortcuts ?? [],\n\t\t}),\n\t\tVariableDecorationExtension,\n\t\t...(config.maxCharacters\n\t\t\t? [CharacterCount.configure({ limit: config.maxCharacters })]\n\t\t\t: []),\n\t\t...(config.characterLimit\n\t\t\t? [CharacterLimitExtension.configure({ limit: config.characterLimit })]\n\t\t\t: []),\n\t\t// Always register SectionDividerNode so the custom <div data-section-divider>\n\t\t// node is part of the schema in every editor instance. This means imperative\n\t\t// callers (setBodyAndSections / setSectionContent) work even when no\n\t\t// `sections` prop was provided at mount time.\n\t\tSectionDividerNode,\n\t];\n\n\tconst mentionExtensions = [\n\t\t...(config.mentions?.agents\n\t\t\t? [\n\t\t\t\t\tbuildAgentMentionExtension(\n\t\t\t\t\t\tconfig.mentions.agents,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t\tconfig.removeMentionTriggerOnDismiss,\n\t\t\t\t\t\tconfig.renderMentionChipIcon,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.mentions?.teams\n\t\t\t? [\n\t\t\t\t\tbuildTeamMentionExtension(\n\t\t\t\t\t\tconfig.mentions.teams,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t\tconfig.removeMentionTriggerOnDismiss,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.slashCommands\n\t\t\t? [\n\t\t\t\t\tbuildSlashCommandExtension(\n\t\t\t\t\t\tconfig.slashCommands,\n\t\t\t\t\t\tconfig.onSlashCommandSelected,\n\t\t\t\t\t\tconfig.renderSlashCommandItem,\n\t\t\t\t\t\tconfig.renderSlashCommandDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t];\n\n\tconst richExtensions = hasRichTypography\n\t\t? [\n\t\t\t\tColor,\n\t\t\t\tHighlight.configure({ multicolor: true }),\n\t\t\t\tFontFamily,\n\t\t\t\tFontSizeExtension,\n\t\t\t\tTextAlign.configure({ types: ['heading', 'paragraph'] }),\n\t\t\t\tSubscript,\n\t\t\t\tSuperscript,\n\t\t\t\tImage,\n\t\t ]\n\t\t: [Color];\n\n\treturn [...base, ...mentionExtensions, ...richExtensions];\n}\n"],"names":["buildExtensions","config","StarterKit","Placeholder","JsonHighlightExtension","SendShortcutExtension","CharacterCount","hasRichPaste","hasRichTypography","allowedMarks","isMark","m","softBreak","HardBreak","isZeroLen","v","ParagraphWithMargin","Paragraph","el","attrs","base","Underline","Link","TextStyle","PasteExtension","PasteNormalizationExtension","VariableDecorationExtension","CharacterLimitExtension","SectionDividerNode","mentionExtensions","buildAgentMentionExtension","buildTeamMentionExtension","buildSlashCommandExtension","richExtensions","Color","Highlight","FontFamily","FontSizeExtension","TextAlign","Subscript","Superscript","Image"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAoFO,SAASA,GAAgBC,GAAyB;AAIxD,MAAIA,EAAO,UAAU;AACpB,WAAO;AAAA,MACNC,EAAW,UAAU;AAAA,QACpB,MAAM;AAAA,QACN,WAAW;AAAA;AAAA;AAAA,QAGX,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,SAAS;AAAA,QACT,YAAY;AAAA,QACZ,aAAa;AAAA,QACb,YAAY;AAAA,QACZ,gBAAgB;AAAA,MAAA,CAChB;AAAA,MACDC,EAAY,UAAU;AAAA,QACrB,aAAaF,EAAO,eAAe;AAAA;AAAA;AAAA,MAAA,CACnC;AAAA,MACDG;AAAA,MACAC,EAAsB,UAAU;AAAA,QAC/B,QAAQJ,EAAO;AAAA,QACf,cAAcA,EAAO;AAAA,QACrB,gBAAgBA,EAAO,aAAa,CAAA;AAAA,MAAC,CACrC;AAAA,MACD,GAAIA,EAAO,gBACR,CAACK,EAAe,UAAU,EAAE,OAAOL,EAAO,cAAA,CAAe,CAAC,IAC1D,CAAA;AAAA,IAAC;AAIN,QAAMM,IAAeN,EAAO,UAAU,aAAa,IAC7CO,IAAoBP,EAAO,UAAU,kBAAkB,IAKvDQ,IAAeR,EAAO,UAAU,cAChCS,IAAS,CAACC,MACf,CAACF,KAAgBA,EAAa,SAASE,CAAC,GAOnCC,IAAYC,EAAU,OAAO;AAAA,IAClC,uBAAuB;AACtB,aAAO;AAAA,QACN,eAAe,MAAM,KAAK,OAAO,SAAS,aAAA;AAAA,MAAa;AAAA,IAEzD;AAAA,EAAA,CACA,GAWKC,IAAY,CAACC,MAClBA,MAAM,OAAOA,MAAM,SAASA,MAAM,SAASA,MAAM,OAC5CC,IAAsBC,EAAU,OAAO;AAAA,IAC5C,gBAAgB;AACf,aAAO;AAAA,QACN,GAAI,KAAK,SAAA,KAAc,CAAA;AAAA,QACvB,QAAQ;AAAA,UACP,SAAS;AAAA,UACT,WAAW,CAACC,MACXJ,EAAUI,EAAG,MAAM,SAAS,KAAKJ,EAAUI,EAAG,MAAM,YAAY,IAC7D,MACA;AAAA,UACJ,YAAY,CAACC,MACZA,EAAM,WAAW,MAAM,EAAE,OAAO,kBAAkB,CAAA;AAAA,QAAC;AAAA,MACrD;AAAA,IAEF;AAAA,EAAA,CACA,GAEKC,IAAO;AAAA;AAAA;AAAA;AAAA,IAIZlB,EAAW,UAAU;AAAA,MACpB,MAAM;AAAA,MACN,WAAW;AAAA,MACX,WAAW;AAAA,MACX,WAAW;AAAA,MACX,MAAMQ,EAAO,MAAM,IAAI,CAAA,IAAK;AAAA,MAC5B,QAAQA,EAAO,QAAQ,IAAI,CAAA,IAAK;AAAA,MAChC,QAAQA,EAAO,QAAQ,IAAI,CAAA,IAAK;AAAA,MAChC,MAAMA,EAAO,MAAM,IAAI,KAAK;AAAA,IAAA,CAC5B;AAAA,IACDM;AAAA,IACAJ;AAAA,IACA,GAAIF,EAAO,WAAW,IAAI,CAACW,CAAS,IAAI,CAAA;AAAA;AAAA;AAAA,IAGxCC,EAAK,OAAO;AAAA,MACX,eAAe,MAAM,CAAA;AAAA,MACrB,eAAe,MAAM,CAAA;AAAA,IAAC,CACtB,EAAE,UAAU;AAAA,MACZ,aAAa;AAAA,MACb,UAAU;AAAA,MACV,aAAa;AAAA,MACb,gBAAgB;AAAA,QACf,KAAK;AAAA,QACL,OAAO;AAAA,MAAA;AAAA,IACR,CACA;AAAA,IACDC;AAAA,IACApB,EAAY,UAAU;AAAA,MACrB,aAAaF,EAAO,eAAe;AAAA,IAAA,CACnC;AAAA;AAAA;AAAA;AAAA,IAID,GAAIA,EAAO,UACR,CAACuB,EAAe,UAAU,EAAE,SAASvB,EAAO,SAAS,CAAC,IACtD,CAAA;AAAA,IACHwB,EAA4B,UAAU;AAAA,MACrC,eAAelB;AAAA;AAAA;AAAA;AAAA,MAIf,WAAW,CAACA;AAAA,IAAA,CACZ;AAAA,IACDF,EAAsB,UAAU;AAAA,MAC/B,QAAQJ,EAAO;AAAA,MACf,cAAcA,EAAO;AAAA,MACrB,gBAAgBA,EAAO,aAAa,CAAA;AAAA,IAAC,CACrC;AAAA,IACDyB;AAAA,IACA,GAAIzB,EAAO,gBACR,CAACK,EAAe,UAAU,EAAE,OAAOL,EAAO,eAAe,CAAC,IAC1D,CAAA;AAAA,IACH,GAAIA,EAAO,iBACR,CAAC0B,EAAwB,UAAU,EAAE,OAAO1B,EAAO,gBAAgB,CAAC,IACpE,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAKH2B;AAAA,EAAA,GAGKC,IAAoB;AAAA,IACzB,GAAI5B,EAAO,UAAU,SAClB;AAAA,MACA6B;AAAA,QACC7B,EAAO,SAAS;AAAA,QAChBA,EAAO;AAAA,QACPA,EAAO;AAAA,QACPA,EAAO;AAAA,QACPA,EAAO;AAAA,QACPA,EAAO;AAAA,MAAA;AAAA,IACR,IAEA,CAAA;AAAA,IACH,GAAIA,EAAO,UAAU,QAClB;AAAA,MACA8B;AAAA,QACC9B,EAAO,SAAS;AAAA,QAChBA,EAAO;AAAA,QACPA,EAAO;AAAA,QACPA,EAAO;AAAA,QACPA,EAAO;AAAA,MAAA;AAAA,IACR,IAEA,CAAA;AAAA,IACH,GAAIA,EAAO,gBACR;AAAA,MACA+B;AAAA,QACC/B,EAAO;AAAA,QACPA,EAAO;AAAA,QACPA,EAAO;AAAA,QACPA,EAAO;AAAA,MAAA;AAAA,IACR,IAEA,CAAA;AAAA,EAAC,GAGCgC,IAAiBzB,IACpB;AAAA,IACA0B;AAAA,IACAC,EAAU,UAAU,EAAE,YAAY,IAAM;AAAA,IACxCC;AAAA,IACAC;AAAA,IACAC,EAAU,UAAU,EAAE,OAAO,CAAC,WAAW,WAAW,GAAG;AAAA,IACvDC;AAAA,IACAC;AAAA,IACAC;AAAA,EAAA,IAEA,CAACP,CAAK;AAET,SAAO,CAAC,GAAGd,GAAM,GAAGS,GAAmB,GAAGI,CAAc;AACzD;"}
1
+ {"version":3,"file":"buildExtensions.js","sources":["../../../../src/editor/extensions/buildExtensions.ts"],"sourcesContent":["import CharacterCount from '@tiptap/extension-character-count';\nimport Color from '@tiptap/extension-color';\nimport FontFamily from '@tiptap/extension-font-family';\nimport HardBreak from '@tiptap/extension-hard-break';\nimport Highlight from '@tiptap/extension-highlight';\nimport Image from '@tiptap/extension-image';\nimport Link from '@tiptap/extension-link';\nimport Paragraph from '@tiptap/extension-paragraph';\nimport Placeholder from '@tiptap/extension-placeholder';\nimport Subscript from '@tiptap/extension-subscript';\nimport Superscript from '@tiptap/extension-superscript';\nimport TextAlign from '@tiptap/extension-text-align';\nimport { TextStyle } from '@tiptap/extension-text-style';\nimport Underline from '@tiptap/extension-underline';\nimport StarterKit from '@tiptap/starter-kit';\nimport type { ReactNode } from 'react';\nimport type {\n\tEditorFeatures,\n\tEditorSnapshot,\n\tKeyboardShortcut,\n\tMentionDropdownRenderProps,\n\tMentionItem,\n\tPasteData,\n\tSlashCommandDropdownRenderProps,\n\tSlashCommandItem,\n} from '../BikEditor.types';\nimport { CharacterLimitExtension } from './characterLimit/CharacterLimitExtension';\nimport { FontSizeExtension } from './FontSizeExtension';\nimport { JsonHighlightExtension } from './jsonHighlight/JsonHighlightExtension';\nimport {\n\tbuildAgentMentionExtension,\n\tbuildTeamMentionExtension,\n} from './mention/MentionExtension';\nimport { PasteExtension } from './paste/PasteExtension';\nimport { PasteNormalizationExtension } from './plainClipboard/PasteNormalizationExtension';\nimport { SectionDividerNode } from './sectionDivider/SectionDividerNode';\nimport { SendShortcutExtension } from './sendShortcut/SendShortcutExtension';\nimport { buildSlashCommandExtension } from './slashCommand/SlashCommandExtension';\nimport { VariableDecorationExtension } from './variable/VariableDecorationExtension';\n\ninterface ExtensionConfig {\n\tfeatures?: EditorFeatures;\n\tplaceholder?: string;\n\tmaxCharacters?: number;\n\t/**\n\t * Soft character limit. Unlike `maxCharacters` (which hard-blocks all input),\n\t * this blocks typing past the limit but ALLOWS paste to overflow, rendering\n\t * the excess characters greyed (`.bik-editor-overflow`). Purely visual +\n\t * typing-guard; the consumer shows the count/error and gates send itself.\n\t */\n\tcharacterLimit?: number;\n\t/**\n\t * Upgrade {@link characterLimit} to a HARD cap: no edit (typing, Enter, paste,\n\t * drop) may grow the content past the limit, while content loaded over the limit\n\t * is preserved and can only shrink. Keeps the same metric as the shown count.\n\t */\n\tcharacterLimitHard?: boolean;\n\thasSections?: boolean;\n\tmentions?: {\n\t\tagents?: { current: MentionItem[] };\n\t\tteams?: { current: MentionItem[] };\n\t};\n\tslashCommands?: { current: SlashCommandItem[] };\n\tonPaste?: (data: PasteData) => boolean | void;\n\tonSend?: (content: EditorSnapshot) => void;\n\tsendShortcut?:\n\t\t| { key: string; modifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'> }\n\t\t| Array<{\n\t\t\t\tkey: string;\n\t\t\t\tmodifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'>;\n\t\t }>;\n\tshortcuts?: KeyboardShortcut[];\n\tonMentionSelected?: (item: MentionItem, char: '@' | '#') => void;\n\tonSlashCommandSelected?: (command: SlashCommandItem) => void;\n\trenderMentionItem?: (item: MentionItem, isActive: boolean) => ReactNode;\n\trenderMentionDropdown?: (props: MentionDropdownRenderProps) => ReactNode;\n\tremoveMentionTriggerOnDismiss?: boolean;\n\trenderMentionChipIcon?: (\n\t\tsource: string | null,\n\t\tkind: string | null,\n\t) => ReactNode;\n\trenderSlashCommandItem?: (\n\t\titem: SlashCommandItem,\n\t\tisActive: boolean,\n\t) => ReactNode;\n\trenderSlashCommandDropdown?: (\n\t\tprops: SlashCommandDropdownRenderProps,\n\t) => ReactNode;\n}\n\nexport function buildExtensions(config: ExtensionConfig) {\n\t// JSON body mode is a dedicated code editor — it deliberately skips the rich\n\t// text stack (mentions, typography, rich paste, sections) and turns the whole\n\t// document into one syntax-highlighted JSON code block, Postman-style.\n\tif (config.features?.jsonMode) {\n\t\treturn [\n\t\t\tStarterKit.configure({\n\t\t\t\tlink: false,\n\t\t\t\tunderline: false,\n\t\t\t\t// Keep codeBlock (the JSON container) and history; drop everything\n\t\t\t\t// that has no meaning inside raw JSON.\n\t\t\t\tbold: false,\n\t\t\t\titalic: false,\n\t\t\t\tstrike: false,\n\t\t\t\tcode: false,\n\t\t\t\theading: false,\n\t\t\t\tbulletList: false,\n\t\t\t\torderedList: false,\n\t\t\t\tblockquote: false,\n\t\t\t\thorizontalRule: false,\n\t\t\t}),\n\t\t\tPlaceholder.configure({\n\t\t\t\tplaceholder: config.placeholder ?? '{\\n \\n}',\n\t\t\t}),\n\t\t\tJsonHighlightExtension,\n\t\t\tSendShortcutExtension.configure({\n\t\t\t\tonSend: config.onSend,\n\t\t\t\tsendShortcut: config.sendShortcut,\n\t\t\t\textraShortcuts: config.shortcuts ?? [],\n\t\t\t}),\n\t\t\t...(config.maxCharacters\n\t\t\t\t? [CharacterCount.configure({ limit: config.maxCharacters })]\n\t\t\t\t: []),\n\t\t];\n\t}\n\n\tconst hasRichPaste = config.features?.richPaste ?? false;\n\tconst hasRichTypography = config.features?.richTypography ?? false;\n\n\t// When allowedMarks is specified only those marks are registered in the schema.\n\t// This disables rendering, keyboard shortcuts AND paste-preservation for the\n\t// excluded marks — all three come for free when the extension is absent.\n\tconst allowedMarks = config.features?.allowedMarks;\n\tconst isMark = (m: 'bold' | 'italic' | 'strike' | 'underline' | 'code') =>\n\t\t!allowedMarks || allowedMarks.includes(m);\n\n\t// Soft line break. Shift-Enter inserts a <br> inside the current block\n\t// (matches Gmail); plain Enter still splits the block into a new paragraph\n\t// via StarterKit's default. HardBreak is registered standalone — like Link\n\t// and Underline above — instead of through StarterKit, so we can bind ONLY\n\t// Shift-Enter here and leave Mod-Enter free for the send shortcut.\n\tconst softBreak = HardBreak.extend({\n\t\taddKeyboardShortcuts() {\n\t\t\treturn {\n\t\t\t\t'Shift-Enter': () => this.editor.commands.setHardBreak(),\n\t\t\t};\n\t\t},\n\t});\n\n\t// Paragraph that preserves a zero block-margin from the source (Gmail-style).\n\t// Most sources (ChatGPT, typed text) use plain <p> → no margin attr → the\n\t// editor/viewer default 1em margin provides the gap. Google-Docs-style\n\t// content uses margin:0 paragraphs and a separate blank-line paragraph for\n\t// spacing; we keep that margin:0 so the blank line — not stacked margins —\n\t// provides a single-line gap, exactly like Gmail. The paste pipeline also\n\t// sets margin:'0' on every paragraph when a paste contains blank-line\n\t// paragraphs (see applyParagraphSpacing), so spacing comes from EITHER the\n\t// margin OR the blank line, never both (no double gap).\n\tconst isZeroLen = (v: string) =>\n\t\tv === '0' || v === '0px' || v === '0pt' || v === '0em';\n\tconst ParagraphWithMargin = Paragraph.extend({\n\t\taddAttributes() {\n\t\t\treturn {\n\t\t\t\t...(this.parent?.() ?? {}),\n\t\t\t\tmargin: {\n\t\t\t\t\tdefault: null,\n\t\t\t\t\tparseHTML: (el: HTMLElement) =>\n\t\t\t\t\t\tisZeroLen(el.style.marginTop) && isZeroLen(el.style.marginBottom)\n\t\t\t\t\t\t\t? '0'\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\trenderHTML: (attrs: { margin?: string | null }) =>\n\t\t\t\t\t\tattrs.margin === '0' ? { style: 'margin: 0px' } : {},\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n\n\tconst base = [\n\t\t// Exclude Link and Underline from StarterKit — TipTap v3 bundles both.\n\t\t// Without this, two copies of each are registered and the StarterKit copy's\n\t\t// click handler calls window.open even when openOnClick: false is set.\n\t\tStarterKit.configure({\n\t\t\tlink: false,\n\t\t\tunderline: false,\n\t\t\thardBreak: false,\n\t\t\tparagraph: false,\n\t\t\tbold: isMark('bold') ? {} : false,\n\t\t\titalic: isMark('italic') ? {} : false,\n\t\t\tstrike: isMark('strike') ? {} : false,\n\t\t\tcode: isMark('code') ? {} : false,\n\t\t}),\n\t\tParagraphWithMargin,\n\t\tsoftBreak,\n\t\t...(isMark('underline') ? [Underline] : []),\n\t\t// Extend Link to strip addPasteRules() and addInputRules() so pasting a\n\t\t// URL never creates a hyperlink regardless of autolink/linkOnPaste values.\n\t\tLink.extend({\n\t\t\taddPasteRules: () => [],\n\t\t\taddInputRules: () => [],\n\t\t}).configure({\n\t\t\topenOnClick: false,\n\t\t\tautolink: false,\n\t\t\tlinkOnPaste: false,\n\t\t\tHTMLAttributes: {\n\t\t\t\trel: 'noopener noreferrer',\n\t\t\t\tclass: 'bik-link',\n\t\t\t},\n\t\t}),\n\t\tTextStyle,\n\t\tPlaceholder.configure({\n\t\t\tplaceholder: config.placeholder ?? 'Type a message...',\n\t\t}),\n\t\t// Consumer paste callback runs first. If it returns true the event is\n\t\t// consumed and neither PlainClipboardExtension nor the editor's default\n\t\t// paste handling will run for that event.\n\t\t...(config.onPaste\n\t\t\t? [PasteExtension.configure({ onPaste: config.onPaste })]\n\t\t\t: []),\n\t\tPasteNormalizationExtension.configure({\n\t\t\tpreserveMarks: hasRichPaste,\n\t\t\t// Only email + signature (richPaste) keep the rich HTML paste pipeline.\n\t\t\t// Every other channel (WhatsApp, IG, FB, comments) forces plain-text\n\t\t\t// paste for BOTH Cmd/Ctrl+V and Cmd/Ctrl+Shift+V — no formatting.\n\t\t\tplainOnly: !hasRichPaste,\n\t\t}),\n\t\tSendShortcutExtension.configure({\n\t\t\tonSend: config.onSend,\n\t\t\tsendShortcut: config.sendShortcut,\n\t\t\textraShortcuts: config.shortcuts ?? [],\n\t\t}),\n\t\tVariableDecorationExtension,\n\t\t...(config.maxCharacters\n\t\t\t? [CharacterCount.configure({ limit: config.maxCharacters })]\n\t\t\t: []),\n\t\t...(config.characterLimit\n\t\t\t? [\n\t\t\t\t\tCharacterLimitExtension.configure({\n\t\t\t\t\t\tlimit: config.characterLimit,\n\t\t\t\t\t\thard: config.characterLimitHard,\n\t\t\t\t\t}),\n\t\t\t ]\n\t\t\t: []),\n\t\t// Always register SectionDividerNode so the custom <div data-section-divider>\n\t\t// node is part of the schema in every editor instance. This means imperative\n\t\t// callers (setBodyAndSections / setSectionContent) work even when no\n\t\t// `sections` prop was provided at mount time.\n\t\tSectionDividerNode,\n\t];\n\n\tconst mentionExtensions = [\n\t\t...(config.mentions?.agents\n\t\t\t? [\n\t\t\t\t\tbuildAgentMentionExtension(\n\t\t\t\t\t\tconfig.mentions.agents,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t\tconfig.removeMentionTriggerOnDismiss,\n\t\t\t\t\t\tconfig.renderMentionChipIcon,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.mentions?.teams\n\t\t\t? [\n\t\t\t\t\tbuildTeamMentionExtension(\n\t\t\t\t\t\tconfig.mentions.teams,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t\tconfig.removeMentionTriggerOnDismiss,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.slashCommands\n\t\t\t? [\n\t\t\t\t\tbuildSlashCommandExtension(\n\t\t\t\t\t\tconfig.slashCommands,\n\t\t\t\t\t\tconfig.onSlashCommandSelected,\n\t\t\t\t\t\tconfig.renderSlashCommandItem,\n\t\t\t\t\t\tconfig.renderSlashCommandDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t];\n\n\tconst richExtensions = hasRichTypography\n\t\t? [\n\t\t\t\tColor,\n\t\t\t\tHighlight.configure({ multicolor: true }),\n\t\t\t\tFontFamily,\n\t\t\t\tFontSizeExtension,\n\t\t\t\tTextAlign.configure({ types: ['heading', 'paragraph'] }),\n\t\t\t\tSubscript,\n\t\t\t\tSuperscript,\n\t\t\t\tImage,\n\t\t ]\n\t\t: [Color];\n\n\treturn [...base, ...mentionExtensions, ...richExtensions];\n}\n"],"names":["buildExtensions","config","StarterKit","Placeholder","JsonHighlightExtension","SendShortcutExtension","CharacterCount","hasRichPaste","hasRichTypography","allowedMarks","isMark","m","softBreak","HardBreak","isZeroLen","v","ParagraphWithMargin","Paragraph","el","attrs","base","Underline","Link","TextStyle","PasteExtension","PasteNormalizationExtension","VariableDecorationExtension","CharacterLimitExtension","SectionDividerNode","mentionExtensions","buildAgentMentionExtension","buildTeamMentionExtension","buildSlashCommandExtension","richExtensions","Color","Highlight","FontFamily","FontSizeExtension","TextAlign","Subscript","Superscript","Image"],"mappings":"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"}
@@ -2,18 +2,35 @@ import { Extension } from '@tiptap/core';
2
2
  export interface CharacterLimitOptions {
3
3
  /** Soft character limit. 0 / undefined disables the extension entirely. */
4
4
  limit: number;
5
+ /**
6
+ * Hard mode. When true, NO edit may push the content past the limit — typing,
7
+ * Enter, drag-drop and any paste the consumer didn't pre-trim are all rejected
8
+ * (a paste is trimmed to fit by the consumer's `onPaste`; anything that still
9
+ * overflows is dropped). Content LOADED over the limit is preserved (initial
10
+ * content isn't a transaction) and stays editable, but can only shrink — so the
11
+ * user can never ADD past the cap, only trim down. Defaults to the soft
12
+ * behaviour (typing blocked, paste allowed to overflow grey) for the CRM.
13
+ */
14
+ hard?: boolean;
5
15
  }
6
16
  /**
7
- * Soft character limit with two behaviours (see CRM design):
17
+ * Character limit with two flavours over the SAME metric (see CRM design):
8
18
  *
19
+ * Soft (default):
9
20
  * 1. Typing is BLOCKED once the content is at/over the limit — `handleTextInput`
10
21
  * rejects the insertion. This never fires for paste (paste flows through the
11
22
  * clipboard pipeline), so paste is free to push the content over the limit.
12
23
  * 2. Every character BEYOND the limit is decorated with `bik-editor-overflow`
13
24
  * (rendered grey) so pasted overflow is visible but clearly marked.
14
25
  *
26
+ * Hard (`hard: true`): additionally a `filterTransaction` rejects ANY transaction
27
+ * that grows the count past the limit (paste, Enter, drop, IME — every path
28
+ * `handleTextInput` misses), while still allowing shrink/no-growth edits so
29
+ * already-over-limit loaded content stays editable. The grey decoration then only
30
+ * ever marks that loaded overflow (a "trim me" hint), never freshly-added text.
31
+ *
15
32
  * Counting mirrors `extractContent` exactly — `doc.textBetween(0, size, '\n\n',
16
- * '\n')` — so the grey boundary lines up with the `characterCount` the consumer
17
- * shows in its error message (paragraph break = 2 chars, soft break = 1 char).
33
+ * '\n')` — so the grey boundary AND the hard cap line up with the `characterCount`
34
+ * the consumer shows (paragraph break = 2 chars, soft break = 1 char).
18
35
  */
19
36
  export declare const CharacterLimitExtension: Extension<CharacterLimitOptions, any>;
@@ -1,55 +1,70 @@
1
- import { Extension as d } from "../../../node_modules/@tiptap/core/dist/index.js";
2
- import { Plugin as h, PluginKey as x } from "@tiptap/pm/state";
3
- import { Decoration as l, DecorationSet as p } from "@tiptap/pm/view";
4
- const k = d.create({
1
+ import { Extension as m } from "../../../node_modules/@tiptap/core/dist/index.js";
2
+ import { Plugin as p, PluginKey as x } from "@tiptap/pm/state";
3
+ import { Decoration as f, DecorationSet as g } from "@tiptap/pm/view";
4
+ const y = m.create({
5
5
  name: "characterLimitOverflow",
6
6
  addOptions() {
7
- return { limit: 0 };
7
+ return { limit: 0, hard: !1 };
8
8
  },
9
9
  addProseMirrorPlugins() {
10
10
  const n = this.options.limit;
11
11
  if (!n || n <= 0) return [];
12
- const f = (r) => r.textBetween(0, r.content.size, `
12
+ const u = this.options.hard, a = (e) => e.textBetween(0, e.content.size, `
13
13
 
14
14
  `, `
15
15
  `).length;
16
16
  return [
17
- new h({
17
+ new p({
18
18
  key: new x("characterLimitOverflow"),
19
+ // Hard cap. Initial content is loaded at editor creation (not a
20
+ // transaction), so it is never filtered here — content loaded over the
21
+ // limit is preserved.
22
+ ...u ? {
23
+ filterTransaction(e, s) {
24
+ if (!e.docChanged) return !0;
25
+ const t = a(e.doc);
26
+ if (t <= n) return !0;
27
+ const r = a(s.doc);
28
+ if (t <= r) return !0;
29
+ if (r > n) return !1;
30
+ const o = t - n, i = e.selection.$head.pos, c = i - o;
31
+ return c <= 0 ? !1 : (e.delete(c, i), a(e.doc) <= n);
32
+ }
33
+ } : {},
19
34
  props: {
20
35
  // Block typed/composed input that would exceed the limit. Replacing
21
36
  // a selection is allowed as long as the result still fits.
22
- handleTextInput(r, s, t, e) {
23
- const i = r.state.doc, o = f(i), c = i.textBetween(s, t, `
37
+ handleTextInput(e, s, t, r) {
38
+ const o = e.state.doc, i = a(o), c = o.textBetween(s, t, `
24
39
 
25
40
  `, `
26
41
  `).length;
27
- return o - c + e.length > n;
42
+ return i - c + r.length > n;
28
43
  },
29
- decorations(r) {
44
+ decorations(e) {
30
45
  const s = [];
31
- let t = 0, e = !1;
32
- return r.doc.descendants((i, o, c, u) => {
33
- if (!e && c && c.type.name === "doc" && u > 0 && (t += 2, t > n && (e = !0)), i.isText && i.text) {
34
- const a = i.text.length;
35
- if (e)
46
+ let t = 0, r = !1;
47
+ return e.doc.descendants((o, i, c, d) => {
48
+ if (!r && c && c.type.name === "doc" && d > 0 && (t += 2, t > n && (r = !0)), o.isText && o.text) {
49
+ const l = o.text.length;
50
+ if (r)
36
51
  s.push(
37
- l.inline(o, o + a, {
52
+ f.inline(i, i + l, {
38
53
  class: "bik-editor-overflow"
39
54
  })
40
55
  );
41
- else if (t + a > n) {
42
- const m = Math.max(0, n - t);
56
+ else if (t + l > n) {
57
+ const h = Math.max(0, n - t);
43
58
  s.push(
44
- l.inline(o + m, o + a, {
59
+ f.inline(i + h, i + l, {
45
60
  class: "bik-editor-overflow"
46
61
  })
47
- ), e = !0;
62
+ ), r = !0;
48
63
  }
49
- t += a;
50
- } else i.type.name === "hardBreak" && (t += 1, !e && t > n && (e = !0));
64
+ t += l;
65
+ } else o.type.name === "hardBreak" && (t += 1, !r && t > n && (r = !0));
51
66
  return !0;
52
- }), p.create(r.doc, s);
67
+ }), g.create(e.doc, s);
53
68
  }
54
69
  }
55
70
  })
@@ -57,6 +72,6 @@ const k = d.create({
57
72
  }
58
73
  });
59
74
  export {
60
- k as CharacterLimitExtension
75
+ y as CharacterLimitExtension
61
76
  };
62
77
  //# sourceMappingURL=CharacterLimitExtension.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CharacterLimitExtension.js","sources":["../../../../../src/editor/extensions/characterLimit/CharacterLimitExtension.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport type { Node as PMNode } from '@tiptap/pm/model';\nimport { Plugin, PluginKey } from '@tiptap/pm/state';\nimport { Decoration, DecorationSet } from '@tiptap/pm/view';\n\nexport interface CharacterLimitOptions {\n\t/** Soft character limit. 0 / undefined disables the extension entirely. */\n\tlimit: number;\n}\n\n/**\n * Soft character limit with two behaviours (see CRM design):\n *\n * 1. Typing is BLOCKED once the content is at/over the limit — `handleTextInput`\n * rejects the insertion. This never fires for paste (paste flows through the\n * clipboard pipeline), so paste is free to push the content over the limit.\n * 2. Every character BEYOND the limit is decorated with `bik-editor-overflow`\n * (rendered grey) so pasted overflow is visible but clearly marked.\n *\n * Counting mirrors `extractContent` exactly — `doc.textBetween(0, size, '\\n\\n',\n * '\\n')` — so the grey boundary lines up with the `characterCount` the consumer\n * shows in its error message (paragraph break = 2 chars, soft break = 1 char).\n */\nexport const CharacterLimitExtension = Extension.create<CharacterLimitOptions>({\n\tname: 'characterLimitOverflow',\n\n\taddOptions() {\n\t\treturn { limit: 0 };\n\t},\n\n\taddProseMirrorPlugins() {\n\t\tconst limit = this.options.limit;\n\t\tif (!limit || limit <= 0) return [];\n\n\t\t// Same metric as BikEditor.utils.extractContent → keeps the grey boundary\n\t\t// and the consumer's characterCount in sync.\n\t\tconst countChars = (doc: PMNode) =>\n\t\t\tdoc.textBetween(0, doc.content.size, '\\n\\n', '\\n').length;\n\n\t\treturn [\n\t\t\tnew Plugin({\n\t\t\t\tkey: new PluginKey('characterLimitOverflow'),\n\t\t\t\tprops: {\n\t\t\t\t\t// Block typed/composed input that would exceed the limit. Replacing\n\t\t\t\t\t// a selection is allowed as long as the result still fits.\n\t\t\t\t\thandleTextInput(view, from, to, text) {\n\t\t\t\t\t\tconst doc = view.state.doc;\n\t\t\t\t\t\tconst current = countChars(doc);\n\t\t\t\t\t\tconst removed = doc.textBetween(from, to, '\\n\\n', '\\n').length;\n\t\t\t\t\t\treturn current - removed + text.length > limit;\n\t\t\t\t\t},\n\t\t\t\t\tdecorations(state) {\n\t\t\t\t\t\tconst decorations: Decoration[] = [];\n\t\t\t\t\t\tlet count = 0;\n\t\t\t\t\t\tlet overflowing = false;\n\n\t\t\t\t\t\tstate.doc.descendants((node, pos, parent, index) => {\n\t\t\t\t\t\t\t// Account for the \"\\n\\n\" separator inserted between top-level\n\t\t\t\t\t\t\t// blocks so the boundary matches textBetween's counting.\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t!overflowing &&\n\t\t\t\t\t\t\t\tparent &&\n\t\t\t\t\t\t\t\tparent.type.name === 'doc' &&\n\t\t\t\t\t\t\t\tindex > 0\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcount += 2;\n\t\t\t\t\t\t\t\tif (count > limit) overflowing = true;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (node.isText && node.text) {\n\t\t\t\t\t\t\t\tconst len = node.text.length;\n\t\t\t\t\t\t\t\tif (overflowing) {\n\t\t\t\t\t\t\t\t\tdecorations.push(\n\t\t\t\t\t\t\t\t\t\tDecoration.inline(pos, pos + len, {\n\t\t\t\t\t\t\t\t\t\t\tclass: 'bik-editor-overflow',\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t} else if (count + len > limit) {\n\t\t\t\t\t\t\t\t\tconst startOffset = Math.max(0, limit - count);\n\t\t\t\t\t\t\t\t\tdecorations.push(\n\t\t\t\t\t\t\t\t\t\tDecoration.inline(pos + startOffset, pos + len, {\n\t\t\t\t\t\t\t\t\t\t\tclass: 'bik-editor-overflow',\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\toverflowing = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcount += len;\n\t\t\t\t\t\t\t} else if (node.type.name === 'hardBreak') {\n\t\t\t\t\t\t\t\t// Soft line break → \"\\n\" (1 char) in textBetween.\n\t\t\t\t\t\t\t\tcount += 1;\n\t\t\t\t\t\t\t\tif (!overflowing && count > limit) overflowing = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\treturn DecorationSet.create(state.doc, decorations);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t];\n\t},\n});\n"],"names":["CharacterLimitExtension","Extension","limit","countChars","doc","Plugin","PluginKey","view","from","to","text","current","removed","state","decorations","count","overflowing","node","pos","parent","index","len","Decoration","startOffset","DecorationSet"],"mappings":";;;AAuBO,MAAMA,IAA0BC,EAAU,OAA8B;AAAA,EAC9E,MAAM;AAAA,EAEN,aAAa;AACZ,WAAO,EAAE,OAAO,EAAA;AAAA,EACjB;AAAA,EAEA,wBAAwB;AACvB,UAAMC,IAAQ,KAAK,QAAQ;AAC3B,QAAI,CAACA,KAASA,KAAS,UAAU,CAAA;AAIjC,UAAMC,IAAa,CAACC,MACnBA,EAAI,YAAY,GAAGA,EAAI,QAAQ,MAAM;AAAA;AAAA,GAAQ;AAAA,CAAI,EAAE;AAEpD,WAAO;AAAA,MACN,IAAIC,EAAO;AAAA,QACV,KAAK,IAAIC,EAAU,wBAAwB;AAAA,QAC3C,OAAO;AAAA;AAAA;AAAA,UAGN,gBAAgBC,GAAMC,GAAMC,GAAIC,GAAM;AACrC,kBAAMN,IAAMG,EAAK,MAAM,KACjBI,IAAUR,EAAWC,CAAG,GACxBQ,IAAUR,EAAI,YAAYI,GAAMC,GAAI;AAAA;AAAA,GAAQ;AAAA,CAAI,EAAE;AACxD,mBAAOE,IAAUC,IAAUF,EAAK,SAASR;AAAA,UAC1C;AAAA,UACA,YAAYW,GAAO;AAClB,kBAAMC,IAA4B,CAAA;AAClC,gBAAIC,IAAQ,GACRC,IAAc;AAElB,mBAAAH,EAAM,IAAI,YAAY,CAACI,GAAMC,GAAKC,GAAQC,MAAU;AAanD,kBATC,CAACJ,KACDG,KACAA,EAAO,KAAK,SAAS,SACrBC,IAAQ,MAERL,KAAS,GACLA,IAAQb,MAAOc,IAAc,MAG9BC,EAAK,UAAUA,EAAK,MAAM;AAC7B,sBAAMI,IAAMJ,EAAK,KAAK;AACtB,oBAAID;AACH,kBAAAF,EAAY;AAAA,oBACXQ,EAAW,OAAOJ,GAAKA,IAAMG,GAAK;AAAA,sBACjC,OAAO;AAAA,oBAAA,CACP;AAAA,kBAAA;AAAA,yBAEQN,IAAQM,IAAMnB,GAAO;AAC/B,wBAAMqB,IAAc,KAAK,IAAI,GAAGrB,IAAQa,CAAK;AAC7C,kBAAAD,EAAY;AAAA,oBACXQ,EAAW,OAAOJ,IAAMK,GAAaL,IAAMG,GAAK;AAAA,sBAC/C,OAAO;AAAA,oBAAA,CACP;AAAA,kBAAA,GAEFL,IAAc;AAAA,gBACf;AACA,gBAAAD,KAASM;AAAA,cACV,MAAA,CAAWJ,EAAK,KAAK,SAAS,gBAE7BF,KAAS,GACL,CAACC,KAAeD,IAAQb,MAAOc,IAAc;AAElD,qBAAO;AAAA,YACR,CAAC,GAEMQ,EAAc,OAAOX,EAAM,KAAKC,CAAW;AAAA,UACnD;AAAA,QAAA;AAAA,MACD,CACA;AAAA,IAAA;AAAA,EAEH;AACD,CAAC;"}
1
+ {"version":3,"file":"CharacterLimitExtension.js","sources":["../../../../../src/editor/extensions/characterLimit/CharacterLimitExtension.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport type { Node as PMNode } from '@tiptap/pm/model';\nimport { Plugin, PluginKey } from '@tiptap/pm/state';\nimport { Decoration, DecorationSet } from '@tiptap/pm/view';\n\nexport interface CharacterLimitOptions {\n\t/** Soft character limit. 0 / undefined disables the extension entirely. */\n\tlimit: number;\n\t/**\n\t * Hard mode. When true, NO edit may push the content past the limit — typing,\n\t * Enter, drag-drop and any paste the consumer didn't pre-trim are all rejected\n\t * (a paste is trimmed to fit by the consumer's `onPaste`; anything that still\n\t * overflows is dropped). Content LOADED over the limit is preserved (initial\n\t * content isn't a transaction) and stays editable, but can only shrink — so the\n\t * user can never ADD past the cap, only trim down. Defaults to the soft\n\t * behaviour (typing blocked, paste allowed to overflow grey) for the CRM.\n\t */\n\thard?: boolean;\n}\n\n/**\n * Character limit with two flavours over the SAME metric (see CRM design):\n *\n * Soft (default):\n * 1. Typing is BLOCKED once the content is at/over the limit — `handleTextInput`\n * rejects the insertion. This never fires for paste (paste flows through the\n * clipboard pipeline), so paste is free to push the content over the limit.\n * 2. Every character BEYOND the limit is decorated with `bik-editor-overflow`\n * (rendered grey) so pasted overflow is visible but clearly marked.\n *\n * Hard (`hard: true`): additionally a `filterTransaction` rejects ANY transaction\n * that grows the count past the limit (paste, Enter, drop, IME — every path\n * `handleTextInput` misses), while still allowing shrink/no-growth edits so\n * already-over-limit loaded content stays editable. The grey decoration then only\n * ever marks that loaded overflow (a \"trim me\" hint), never freshly-added text.\n *\n * Counting mirrors `extractContent` exactly — `doc.textBetween(0, size, '\\n\\n',\n * '\\n')` — so the grey boundary AND the hard cap line up with the `characterCount`\n * the consumer shows (paragraph break = 2 chars, soft break = 1 char).\n */\nexport const CharacterLimitExtension = Extension.create<CharacterLimitOptions>({\n\tname: 'characterLimitOverflow',\n\n\taddOptions() {\n\t\treturn { limit: 0, hard: false };\n\t},\n\n\taddProseMirrorPlugins() {\n\t\tconst limit = this.options.limit;\n\t\tif (!limit || limit <= 0) return [];\n\t\tconst hard = this.options.hard;\n\n\t\t// Same metric as BikEditor.utils.extractContent → keeps the grey boundary\n\t\t// and the consumer's characterCount in sync.\n\t\tconst countChars = (doc: PMNode) =>\n\t\t\tdoc.textBetween(0, doc.content.size, '\\n\\n', '\\n').length;\n\n\t\treturn [\n\t\t\tnew Plugin({\n\t\t\t\tkey: new PluginKey('characterLimitOverflow'),\n\t\t\t\t// Hard cap. Initial content is loaded at editor creation (not a\n\t\t\t\t// transaction), so it is never filtered here — content loaded over the\n\t\t\t\t// limit is preserved.\n\t\t\t\t...(hard\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tfilterTransaction(tr, state) {\n\t\t\t\t\t\t\t\tif (!tr.docChanged) return true;\n\t\t\t\t\t\t\t\tconst next = countChars(tr.doc);\n\t\t\t\t\t\t\t\tif (next <= limit) return true;\n\t\t\t\t\t\t\t\tconst prev = countChars(state.doc);\n\t\t\t\t\t\t\t\t// Shrinking / no net growth always passes, so content already\n\t\t\t\t\t\t\t\t// over the limit can be trimmed DOWN (never up).\n\t\t\t\t\t\t\t\tif (next <= prev) return true;\n\t\t\t\t\t\t\t\t// Already over → block any growth outright; never delete the\n\t\t\t\t\t\t\t\t// user's existing over-limit content to make room.\n\t\t\t\t\t\t\t\tif (prev > limit) return false;\n\t\t\t\t\t\t\t\t// A single edit crossed under→over — a paste. Keep the part that\n\t\t\t\t\t\t\t\t// fits and drop the excess tail (paste truncates to the cap)\n\t\t\t\t\t\t\t\t// instead of rejecting the whole paste. Deleting `over` POSITIONS\n\t\t\t\t\t\t\t\t// ending at the caret removes >= `over` characters (a block break\n\t\t\t\t\t\t\t\t// is 2 chars but 1 position), so the result lands <= limit; the\n\t\t\t\t\t\t\t\t// re-check blocks the rare case it somehow didn't.\n\t\t\t\t\t\t\t\tconst over = next - limit;\n\t\t\t\t\t\t\t\tconst head = tr.selection.$head.pos;\n\t\t\t\t\t\t\t\tconst from = head - over;\n\t\t\t\t\t\t\t\tif (from <= 0) return false;\n\t\t\t\t\t\t\t\ttr.delete(from, head);\n\t\t\t\t\t\t\t\treturn countChars(tr.doc) <= limit;\n\t\t\t\t\t\t\t},\n\t\t\t\t\t }\n\t\t\t\t\t: {}),\n\t\t\t\tprops: {\n\t\t\t\t\t// Block typed/composed input that would exceed the limit. Replacing\n\t\t\t\t\t// a selection is allowed as long as the result still fits.\n\t\t\t\t\thandleTextInput(view, from, to, text) {\n\t\t\t\t\t\tconst doc = view.state.doc;\n\t\t\t\t\t\tconst current = countChars(doc);\n\t\t\t\t\t\tconst removed = doc.textBetween(from, to, '\\n\\n', '\\n').length;\n\t\t\t\t\t\treturn current - removed + text.length > limit;\n\t\t\t\t\t},\n\t\t\t\t\tdecorations(state) {\n\t\t\t\t\t\tconst decorations: Decoration[] = [];\n\t\t\t\t\t\tlet count = 0;\n\t\t\t\t\t\tlet overflowing = false;\n\n\t\t\t\t\t\tstate.doc.descendants((node, pos, parent, index) => {\n\t\t\t\t\t\t\t// Account for the \"\\n\\n\" separator inserted between top-level\n\t\t\t\t\t\t\t// blocks so the boundary matches textBetween's counting.\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t!overflowing &&\n\t\t\t\t\t\t\t\tparent &&\n\t\t\t\t\t\t\t\tparent.type.name === 'doc' &&\n\t\t\t\t\t\t\t\tindex > 0\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tcount += 2;\n\t\t\t\t\t\t\t\tif (count > limit) overflowing = true;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (node.isText && node.text) {\n\t\t\t\t\t\t\t\tconst len = node.text.length;\n\t\t\t\t\t\t\t\tif (overflowing) {\n\t\t\t\t\t\t\t\t\tdecorations.push(\n\t\t\t\t\t\t\t\t\t\tDecoration.inline(pos, pos + len, {\n\t\t\t\t\t\t\t\t\t\t\tclass: 'bik-editor-overflow',\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t} else if (count + len > limit) {\n\t\t\t\t\t\t\t\t\tconst startOffset = Math.max(0, limit - count);\n\t\t\t\t\t\t\t\t\tdecorations.push(\n\t\t\t\t\t\t\t\t\t\tDecoration.inline(pos + startOffset, pos + len, {\n\t\t\t\t\t\t\t\t\t\t\tclass: 'bik-editor-overflow',\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\toverflowing = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcount += len;\n\t\t\t\t\t\t\t} else if (node.type.name === 'hardBreak') {\n\t\t\t\t\t\t\t\t// Soft line break → \"\\n\" (1 char) in textBetween.\n\t\t\t\t\t\t\t\tcount += 1;\n\t\t\t\t\t\t\t\tif (!overflowing && count > limit) overflowing = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\treturn DecorationSet.create(state.doc, decorations);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t];\n\t},\n});\n"],"names":["CharacterLimitExtension","Extension","limit","hard","countChars","doc","Plugin","PluginKey","tr","state","next","prev","over","head","from","view","to","text","current","removed","decorations","count","overflowing","node","pos","parent","index","len","Decoration","startOffset","DecorationSet"],"mappings":";;;AAwCO,MAAMA,IAA0BC,EAAU,OAA8B;AAAA,EAC9E,MAAM;AAAA,EAEN,aAAa;AACZ,WAAO,EAAE,OAAO,GAAG,MAAM,GAAA;AAAA,EAC1B;AAAA,EAEA,wBAAwB;AACvB,UAAMC,IAAQ,KAAK,QAAQ;AAC3B,QAAI,CAACA,KAASA,KAAS,UAAU,CAAA;AACjC,UAAMC,IAAO,KAAK,QAAQ,MAIpBC,IAAa,CAACC,MACnBA,EAAI,YAAY,GAAGA,EAAI,QAAQ,MAAM;AAAA;AAAA,GAAQ;AAAA,CAAI,EAAE;AAEpD,WAAO;AAAA,MACN,IAAIC,EAAO;AAAA,QACV,KAAK,IAAIC,EAAU,wBAAwB;AAAA;AAAA;AAAA;AAAA,QAI3C,GAAIJ,IACD;AAAA,UACA,kBAAkBK,GAAIC,GAAO;AAC5B,gBAAI,CAACD,EAAG,WAAY,QAAO;AAC3B,kBAAME,IAAON,EAAWI,EAAG,GAAG;AAC9B,gBAAIE,KAAQR,EAAO,QAAO;AAC1B,kBAAMS,IAAOP,EAAWK,EAAM,GAAG;AAGjC,gBAAIC,KAAQC,EAAM,QAAO;AAGzB,gBAAIA,IAAOT,EAAO,QAAO;AAOzB,kBAAMU,IAAOF,IAAOR,GACdW,IAAOL,EAAG,UAAU,MAAM,KAC1BM,IAAOD,IAAOD;AACpB,mBAAIE,KAAQ,IAAU,MACtBN,EAAG,OAAOM,GAAMD,CAAI,GACbT,EAAWI,EAAG,GAAG,KAAKN;AAAA,UAC9B;AAAA,QAAA,IAEA,CAAA;AAAA,QACH,OAAO;AAAA;AAAA;AAAA,UAGN,gBAAgBa,GAAMD,GAAME,GAAIC,GAAM;AACrC,kBAAMZ,IAAMU,EAAK,MAAM,KACjBG,IAAUd,EAAWC,CAAG,GACxBc,IAAUd,EAAI,YAAYS,GAAME,GAAI;AAAA;AAAA,GAAQ;AAAA,CAAI,EAAE;AACxD,mBAAOE,IAAUC,IAAUF,EAAK,SAASf;AAAA,UAC1C;AAAA,UACA,YAAYO,GAAO;AAClB,kBAAMW,IAA4B,CAAA;AAClC,gBAAIC,IAAQ,GACRC,IAAc;AAElB,mBAAAb,EAAM,IAAI,YAAY,CAACc,GAAMC,GAAKC,GAAQC,MAAU;AAanD,kBATC,CAACJ,KACDG,KACAA,EAAO,KAAK,SAAS,SACrBC,IAAQ,MAERL,KAAS,GACLA,IAAQnB,MAAOoB,IAAc,MAG9BC,EAAK,UAAUA,EAAK,MAAM;AAC7B,sBAAMI,IAAMJ,EAAK,KAAK;AACtB,oBAAID;AACH,kBAAAF,EAAY;AAAA,oBACXQ,EAAW,OAAOJ,GAAKA,IAAMG,GAAK;AAAA,sBACjC,OAAO;AAAA,oBAAA,CACP;AAAA,kBAAA;AAAA,yBAEQN,IAAQM,IAAMzB,GAAO;AAC/B,wBAAM2B,IAAc,KAAK,IAAI,GAAG3B,IAAQmB,CAAK;AAC7C,kBAAAD,EAAY;AAAA,oBACXQ,EAAW,OAAOJ,IAAMK,GAAaL,IAAMG,GAAK;AAAA,sBAC/C,OAAO;AAAA,oBAAA,CACP;AAAA,kBAAA,GAEFL,IAAc;AAAA,gBACf;AACA,gBAAAD,KAASM;AAAA,cACV,MAAA,CAAWJ,EAAK,KAAK,SAAS,gBAE7BF,KAAS,GACL,CAACC,KAAeD,IAAQnB,MAAOoB,IAAc;AAElD,qBAAO;AAAA,YACR,CAAC,GAEMQ,EAAc,OAAOrB,EAAM,KAAKW,CAAW;AAAA,UACnD;AAAA,QAAA;AAAA,MACD,CACA;AAAA,IAAA;AAAA,EAEH;AACD,CAAC;"}