UNPKG

payload-lexical-typography

Version:
1 lines 118 kB
{"version":3,"sources":["../src/client.ts","../src/features/textColor/feature.client.tsx","../src/features/textColor/command.ts","../src/features/textColor/components/TextColorDropdown.tsx","../src/features/textColor/components/TextColorPicker.tsx","../src/utils/usePreventInlineToolbarClose.ts","../src/features/textColor/components/TextColorIcon.tsx","../src/utils/getSelection.ts","../src/features/textSize/feature.client.tsx","../src/features/textSize/command.ts","../src/features/textSize/components/TextSizeDropdown.tsx","../src/features/textSize/components/TextSizePicker.tsx","../src/features/textSize/components/TextSizeIcon.tsx","../src/features/textLetterSpacing/feature.client.tsx","../src/features/textLetterSpacing/command.ts","../src/features/textLetterSpacing/components/TextLetterSpacingDropdown.tsx","../src/features/textLetterSpacing/components/TextLetterSpacingPicker.tsx","../src/features/textLetterSpacing/components/TextLetterSpacingIcon.tsx","../src/features/textLineHeight/feature.client.tsx","../src/features/textLineHeight/command.ts","../src/features/textLineHeight/components/TextLineHeightDropdown.tsx","../src/features/textLineHeight/components/TextLineHeightPicker.tsx","../src/features/textLineHeight/components/TextLineHeightIcon.tsx","../src/features/textFontFamily/feature.client.tsx","../src/features/textFontFamily/command.ts","../src/features/textFontFamily/components/TextFontFamilyDropdown.tsx","../src/features/textFontFamily/components/TextFontFamilyPicker.tsx","../src/features/textFontFamily/components/TextFontFamilyIcon.tsx"],"sourcesContent":["\"use client\";\n\nexport { TextColorClientFeature } from \"./features/textColor/feature.client\";\nexport { TextSizeClientFeature } from \"./features/textSize/feature.client\";\nexport { TextLetterSpacingClientFeature } from \"./features/textLetterSpacing/feature.client\";\nexport { TextLineHeightClientFeature } from \"./features/textLineHeight/feature.client\";\nexport { TextFontFamilyClientFeature } from \"./features/textFontFamily/feature.client\";\n","\"use client\";\n\nimport { type ToolbarGroup, type ToolbarGroupItem } from \"@payloadcms/richtext-lexical\";\nimport { createClientFeature } from \"@payloadcms/richtext-lexical/client\";\nimport { COMMAND_PRIORITY_CRITICAL, type BaseSelection } from \"@payloadcms/richtext-lexical/lexical\";\nimport { useLexicalComposerContext } from \"@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext\";\nimport {\n $getSelectionStyleValueForProperty,\n $patchStyleText,\n} from \"@payloadcms/richtext-lexical/lexical/selection\";\n\nimport { useEffect } from \"react\";\n\nimport { TEXT_COLOR_COMMAND } from \"./command\";\nimport { TextColorDropdown } from \"./components/TextColorDropdown\";\nimport { TextColorIcon } from \"./components/TextColorIcon\";\n\nimport { getSelection } from \"../../utils/getSelection\";\n\nexport type TextColorFeatureProps = {\n colors?: string[] | { value: string; label: string }[];\n colorPicker?: boolean;\n hideAttribution?: boolean;\n listView?: boolean;\n};\n\nexport type TextColorItem = ToolbarGroupItem & {\n command: Record<string, unknown>;\n current: () => string | null;\n} & TextColorFeatureProps;\n\nexport const TextColorClientFeature = createClientFeature<TextColorFeatureProps, TextColorItem>(\n ({ props }) => {\n const colors =\n props?.colors && props?.colors.length > 0\n ? props.colors\n : [\"#FF0000\", \"#00FF00\", \"#0000FF\", \"#FFFF00\", \"#FF00FF\"];\n\n const DropdownComponent: ToolbarGroup = {\n type: \"dropdown\",\n ChildComponent: TextColorIcon,\n isEnabled({ selection }: { selection: BaseSelection }) {\n return !!getSelection(selection);\n },\n items: [\n {\n Component: () => {\n const [editor] = useLexicalComposerContext();\n return TextColorDropdown({\n editor,\n item: {\n command: TEXT_COLOR_COMMAND,\n current() {\n const selection = getSelection();\n return selection ? $getSelectionStyleValueForProperty(selection, \"color\", \"\") : null;\n },\n colors,\n listView: props?.listView,\n hideAttribution: props?.hideAttribution,\n colorPicker: props?.colorPicker,\n key: \"textColor\",\n },\n });\n },\n key: \"textColor\",\n },\n ],\n key: \"textColorDropdown\",\n order: 60,\n };\n\n return {\n plugins: [\n {\n Component: () => {\n const [editor] = useLexicalComposerContext();\n\n useEffect(() => {\n return editor.registerCommand(\n TEXT_COLOR_COMMAND,\n (payload) => {\n editor.update(() => {\n const selection = getSelection();\n if (selection) {\n $patchStyleText(selection, { color: payload.color || \"\" });\n }\n });\n return true;\n },\n COMMAND_PRIORITY_CRITICAL,\n );\n }, [editor]);\n\n return null;\n },\n position: \"normal\",\n },\n ],\n toolbarFixed: {\n groups: [DropdownComponent],\n },\n toolbarInline: {\n groups: [DropdownComponent],\n },\n };\n },\n);\n","import { createCommand } from \"@payloadcms/richtext-lexical/lexical\";\n\nexport const TEXT_COLOR_COMMAND = createCommand<{ color: string }>(\"TEXT_COLOR_COMMAND\");\n","import { type LexicalEditor } from \"@payloadcms/richtext-lexical/lexical\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { TextColorPicker } from \"./TextColorPicker\";\n\nimport { type TextColorItem } from \"../feature.client\";\n\nexport const TextColorDropdown = ({ editor, item }: { editor: LexicalEditor; item: TextColorItem }) => {\n const [activeColor, setActiveColor] = useState<string>(\"\");\n\n const onChange = (color: string) => {\n setActiveColor(color || \"\");\n };\n\n const applyColor = (color?: string) => {\n editor.dispatchCommand(item.command, { color: color ?? activeColor });\n };\n\n const handleReset = () => {\n editor.dispatchCommand(item.command, { color: \"\" });\n setActiveColor(\"\");\n };\n\n useEffect(() => {\n editor.read(() => {\n const current = item.current ? item.current() : null;\n if (current) setActiveColor(current);\n });\n }, [editor, item]);\n\n return (\n <TextColorPicker\n color={activeColor}\n applyColor={applyColor}\n onChange={onChange}\n colors={item.colors}\n hideAttribution={item.hideAttribution}\n colorPicker={item.colorPicker}\n listView={item.listView}\n handleReset={handleReset}\n />\n );\n};\n","import { useEffect, useState } from \"react\";\nimport { HexColorPicker } from \"react-colorful\";\n\nimport { usePreventInlineToolbarClose } from \"../../../utils/usePreventInlineToolbarClose\";\nimport { type TextColorFeatureProps } from \"../feature.client\";\n\nconst injectStyles = () => {\n const style = document.createElement(\"style\");\n style.innerHTML = `\n div.react-colorful .react-colorful__pointer {\n width: 20px;\n height: 20px;\n }\n div.react-colorful .react-colorful__hue {\n height: 22px;\n }\n `;\n document.head.appendChild(style);\n};\n\nexport const TextColorPicker = ({\n color,\n applyColor,\n onChange,\n colors = [],\n hideAttribution = false,\n colorPicker = true,\n listView,\n handleReset,\n}: {\n color: string;\n applyColor: (color?: string) => void;\n onChange: (color: string) => void;\n handleReset: () => void;\n} & TextColorFeatureProps) => {\n const { containerProps, inputProps } = usePreventInlineToolbarClose();\n const [predefinedColors, setPredefinedColors] = useState(true);\n\n useEffect(() => {\n injectStyles();\n }, []);\n\n const isGridView = listView === false || (listView !== true && typeof colors[0] === \"string\");\n\n return (\n <div\n {...containerProps}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n maxWidth: \"165px\",\n width: \"100%\",\n }}\n >\n {!predefinedColors && colorPicker ? (\n <div\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n }}\n style={{\n width: \"100%\",\n paddingTop: \"8px\",\n paddingLeft: \"8px\",\n paddingRight: \"8px\",\n paddingBottom: \"0px\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n }}\n >\n <HexColorPicker\n style={{\n maxWidth: \"100%\",\n height: \"min-content\",\n aspectRatio: \"1\",\n }}\n color={color}\n onChange={onChange}\n />\n <div className=\"field-type text\">\n <input\n style={{\n width: \"100%\",\n margin: \"8px 0\",\n height: \"25px\",\n paddingTop: \"0\",\n paddingBottom: \"1px\",\n paddingLeft: \"10px\",\n }}\n type=\"text\"\n value={color}\n onChange={(e) => {\n e.preventDefault();\n e.stopPropagation();\n onChange(e.target.value);\n }}\n {...inputProps}\n />\n </div>\n </div>\n ) : (\n <div\n style={{\n display: isGridView ? \"grid\" : \"flex\",\n gridTemplateColumns: isGridView ? \"repeat(5, 1fr)\" : undefined,\n flexDirection: isGridView ? undefined : \"column\",\n gap: isGridView ? \"4px\" : \"6px\",\n padding: \"8px\",\n overflowY: isGridView ? undefined : \"auto\",\n maxHeight: isGridView ? undefined : \"266px\",\n }}\n >\n {colors.map((unionC) => {\n const c = typeof unionC === \"string\" ? unionC : unionC.value;\n return (\n <button\n key={c}\n onClick={() => {\n applyColor(c);\n }}\n style={{\n display: \"flex\",\n gap: isGridView ? \"4px\" : \"6px\",\n alignItems: \"center\",\n cursor: \"pointer\",\n background: \"transparent\",\n border: \"none\",\n padding: \"0\",\n }}\n >\n <div\n style={{\n backgroundColor: c,\n width: \"26px\",\n height: \"26px\",\n borderRadius: \"50%\",\n border:\n color === c\n ? \"2px solid var(--theme-elevation-900)\"\n : \"2px solid var(--theme-elevation-150)\",\n }}\n />\n {!isGridView && <span>{typeof unionC === \"string\" ? unionC : unionC.label}</span>}\n </button>\n );\n })}\n <button\n onClick={() => handleReset()}\n style={{\n display: \"flex\",\n gap: isGridView ? \"4px\" : \"6px\",\n alignItems: \"center\",\n cursor: \"pointer\",\n background: \"transparent\",\n border: \"none\",\n padding: \"0\",\n }}\n >\n <div\n style={{\n width: \"26px\",\n height: \"26px\",\n borderRadius: \"50%\",\n border: \"2px solid var(--theme-elevation-150)\",\n position: \"relative\",\n cursor: \"pointer\",\n }}\n >\n <div\n style={{\n position: \"absolute\",\n width: \"100%\",\n height: \"2px\",\n backgroundColor: \"#FF0000\",\n top: \"50%\",\n left: \"50%\",\n transform: \"translate(-50%,-50%) rotate(45deg)\",\n }}\n />\n </div>\n {!isGridView && <span>Reset</span>}\n </button>\n </div>\n )}\n {!predefinedColors && (\n <div style={{ display: \"flex\", gap: \"8px\" }}>\n <button\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n handleReset();\n }}\n className=\"btn btn--icon-style-without-border btn--size-small btn--withoutPopup btn--style-pill btn--withoutPopup\"\n style={{ marginLeft: \"auto\", margin: \"0\", cursor: \"pointer\", flex: 1 }}\n >\n Reset\n </button>\n <button\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n applyColor();\n }}\n className=\"btn btn--icon-style-without-border btn--size-small btn--withoutPopup btn--style-pill btn--withoutPopup\"\n style={{ marginLeft: \"auto\", margin: \"0\", cursor: \"pointer\", flex: 1 }}\n >\n Apply\n </button>\n </div>\n )}\n <div\n style={{\n width: \"100%\",\n padding: \"8px\",\n display: \"flex\",\n gap: \"8px\",\n flexDirection: \"column\",\n alignItems: \"center\",\n }}\n >\n {colorPicker && (\n <button\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n setPredefinedColors((prev) => !prev);\n }}\n className=\"btn btn--icon-style-without-border btn--size-small btn--withoutPopup btn--style-pill btn--withoutPopup\"\n style={{\n margin: 0,\n }}\n >\n {predefinedColors ? \"Color picker\" : \"Predefined colors\"}\n </button>\n )}\n {!hideAttribution && (\n <p\n style={{\n color: \"var(--theme-elevation-650)\",\n fontSize: \"10px\",\n }}\n >\n Made with ❤️ by{\" \"}\n <a target=\"_blank\" href=\"https://github.com/AdrianMaj\">\n @AdrianMaj\n </a>\n </p>\n )}\n </div>\n </div>\n );\n};\n","import { useEffect, useRef } from \"react\";\n\nexport const usePreventInlineToolbarClose = () => {\n const containerRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const handleSelectionChange = (e: Event) => {\n const activeElement = document.activeElement;\n if (activeElement && container.contains(activeElement)) {\n e.stopImmediatePropagation();\n }\n };\n\n const handleMouseUp = (e: MouseEvent) => {\n if (container.contains(e.target as Node)) {\n e.stopImmediatePropagation();\n }\n };\n\n document.addEventListener(\"selectionchange\", handleSelectionChange, true);\n document.addEventListener(\"mouseup\", handleMouseUp, true);\n\n return () => {\n document.removeEventListener(\"selectionchange\", handleSelectionChange, true);\n document.removeEventListener(\"mouseup\", handleMouseUp, true);\n };\n }, []);\n\n const handleInputInteraction = (e: React.FocusEvent | React.MouseEvent) => {\n e.stopPropagation();\n\n const selection = window.getSelection();\n if (selection && selection.rangeCount > 0) {\n const range = selection.getRangeAt(0);\n (e.currentTarget as HTMLElement).dataset.editorRange = JSON.stringify({\n startContainer: range.startContainer.textContent,\n startOffset: range.startOffset,\n endContainer: range.endContainer.textContent,\n endOffset: range.endOffset,\n });\n }\n };\n\n const handleInputBlur = (e: React.FocusEvent) => {\n setTimeout(() => {\n const activeElement = document.activeElement;\n if (!containerRef.current?.contains(activeElement)) {\n const rangeData = (e.currentTarget as HTMLElement).dataset.editorRange;\n if (rangeData) {\n delete (e.currentTarget as HTMLElement).dataset.editorRange;\n }\n }\n }, 10);\n };\n\n const containerProps = {\n ref: containerRef,\n onMouseDown: (e: React.MouseEvent) => e.stopPropagation(),\n onMouseMove: (e: React.MouseEvent) => e.stopPropagation(),\n };\n\n const inputProps = {\n onFocus: handleInputInteraction,\n onBlur: handleInputBlur,\n onMouseDown: handleInputInteraction,\n };\n\n return {\n containerProps,\n inputProps,\n };\n};\n","import { COMMAND_PRIORITY_CRITICAL, SELECTION_CHANGE_COMMAND } from \"@payloadcms/richtext-lexical/lexical\";\nimport { useLexicalComposerContext } from \"@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext\";\nimport {\n $getSelectionStyleValueForProperty,\n $patchStyleText,\n} from \"@payloadcms/richtext-lexical/lexical/selection\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { getSelection } from \"../../../utils/getSelection\";\nimport { TEXT_COLOR_COMMAND } from \"../command\";\n\nexport const TextColorIcon = () => {\n const [color, setColor] = useState<string>(\"\");\n const [editor] = useLexicalComposerContext();\n\n const updateCurrentColor = () => {\n const selection = getSelection();\n if (selection) setColor($getSelectionStyleValueForProperty(selection, \"color\", \"\"));\n return false;\n };\n\n useEffect(() => {\n return editor.registerCommand(\n TEXT_COLOR_COMMAND,\n (payload) => {\n setColor(payload.color);\n editor.update(() => {\n const selection = getSelection();\n if (selection) $patchStyleText(selection, { color: payload.color || \"\" });\n });\n return false;\n },\n COMMAND_PRIORITY_CRITICAL,\n );\n }, [editor]);\n\n useEffect(() => {\n setTimeout(() => {\n return editor.read(updateCurrentColor);\n });\n return editor.registerCommand(SELECTION_CHANGE_COMMAND, updateCurrentColor, COMMAND_PRIORITY_CRITICAL);\n }, [editor]);\n\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M4 20h16\" style={{ color }} />\n <path d=\"m6 16 6-12 6 12\" />\n <path d=\"M8 12h8\" />\n </svg>\n );\n};\n","import { $getSelection, $isRangeSelection } from \"@payloadcms/richtext-lexical/lexical\";\n\nexport const getSelection = (selection = $getSelection()) => {\n if ($isRangeSelection(selection)) {\n return selection;\n }\n return null;\n};\n","\"use client\";\n\nimport { type ToolbarGroup, type ToolbarGroupItem } from \"@payloadcms/richtext-lexical\";\nimport { createClientFeature } from \"@payloadcms/richtext-lexical/client\";\nimport { COMMAND_PRIORITY_CRITICAL, type BaseSelection } from \"@payloadcms/richtext-lexical/lexical\";\nimport { useLexicalComposerContext } from \"@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext\";\nimport {\n $getSelectionStyleValueForProperty,\n $patchStyleText,\n} from \"@payloadcms/richtext-lexical/lexical/selection\";\n\nimport { useEffect } from \"react\";\n\nimport { TEXT_SIZE_COMMAND } from \"./command\";\nimport { Dropdown } from \"./components/TextSizeDropdown\";\nimport { TextSizeIcon } from \"./components/TextSizeIcon\";\n\nimport { getSelection } from \"../../utils/getSelection\";\n\nexport type TextSizeFeatureProps = {\n hideAttribution?: boolean;\n sizes?: { value: string; label: string }[];\n method?: \"replace\" | \"combine\";\n customSize?: boolean;\n scroll?: boolean;\n};\n\nexport type TextSizeItem = ToolbarGroupItem & {\n command: Record<string, unknown>;\n current: () => string | null;\n} & TextSizeFeatureProps;\n\nexport const TextSizeClientFeature = createClientFeature<TextSizeFeatureProps, TextSizeItem>(({ props }) => {\n const DropdownComponent: ToolbarGroup = {\n type: \"dropdown\",\n ChildComponent: TextSizeIcon,\n isEnabled({ selection }: { selection: BaseSelection }) {\n return !!getSelection(selection);\n },\n items: [\n {\n Component: () => {\n const [editor] = useLexicalComposerContext();\n return Dropdown({\n editor,\n item: {\n command: TEXT_SIZE_COMMAND,\n current() {\n const selection = getSelection();\n return selection ? $getSelectionStyleValueForProperty(selection, \"font-size\", \"\") : null;\n },\n hideAttribution: props?.hideAttribution,\n sizes: props?.sizes,\n method: props?.method,\n scroll: props?.scroll,\n customSize: props?.customSize,\n key: \"textSize\",\n },\n });\n },\n key: \"textSize\",\n },\n ],\n key: \"textSizeDropdown\",\n order: 60,\n };\n\n return {\n plugins: [\n {\n Component: () => {\n const [editor] = useLexicalComposerContext();\n\n useEffect(() => {\n return editor.registerCommand(\n TEXT_SIZE_COMMAND,\n (payload) => {\n editor.update(() => {\n const selection = getSelection();\n if (selection) {\n $patchStyleText(selection, { \"font-size\": payload.size || \"\" });\n }\n });\n return true;\n },\n COMMAND_PRIORITY_CRITICAL,\n );\n }, [editor]);\n\n return null;\n },\n position: \"normal\",\n },\n ],\n toolbarFixed: {\n groups: [DropdownComponent],\n },\n toolbarInline: {\n groups: [DropdownComponent],\n },\n };\n});\n","import { createCommand } from \"@payloadcms/richtext-lexical/lexical\";\n\nexport const TEXT_SIZE_COMMAND = createCommand<{ size: string }>(\"TEXT_SIZE_COMMAND\");\n","import { type LexicalEditor } from \"@payloadcms/richtext-lexical/lexical\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { SizePicker } from \"./TextSizePicker\";\n\nimport { type TextSizeItem } from \"../feature.client\";\n\nexport const Dropdown = ({ editor, item }: { editor: LexicalEditor; item: TextSizeItem }) => {\n const [activeSize, setActiveSize] = useState<string>(\"\");\n\n const onChange = (size: string) => {\n editor.dispatchCommand(item.command, { size });\n setActiveSize(size || \"\");\n };\n\n useEffect(() => {\n editor.read(() => {\n const current = item.current ? item.current() : null;\n if (current) setActiveSize(current);\n });\n }, [editor, item]);\n\n return (\n <SizePicker\n size={activeSize}\n onChange={onChange}\n hideAttribution={item.hideAttribution}\n method={item.method}\n scroll={item.scroll}\n sizes={item.sizes}\n customSize={item.customSize}\n />\n );\n};\n","import { useState, useEffect, useRef, type ChangeEvent } from \"react\";\n\nimport { usePreventInlineToolbarClose } from \"../../../utils/usePreventInlineToolbarClose\";\nimport { type TextSizeFeatureProps } from \"../feature.client\";\n\nexport const SizePicker = ({\n size,\n onChange,\n hideAttribution,\n sizes,\n method = \"replace\",\n scroll = true,\n customSize = true,\n}: {\n size: string;\n onChange: (size: string) => void;\n} & TextSizeFeatureProps) => {\n const isEditingRef = useRef(false);\n const { containerProps, inputProps } = usePreventInlineToolbarClose();\n\n const defaultSizeOptions = [\n { value: \"0.875rem\", label: \"Small\" },\n { value: \"1.25rem\", label: \"Normal\" },\n { value: \"1.875rem\", label: \"Large\" },\n { value: \"3rem\", label: \"Huge\" },\n ];\n\n const options =\n method === \"replace\" ? (sizes ?? defaultSizeOptions) : [...defaultSizeOptions, ...(sizes ?? [])];\n\n const units = [\"px\", \"rem\", \"em\", \"vh\", \"vw\", \"%\"];\n\n const [displayValue, setDisplayValue] = useState(size || \"\");\n const [appliedValue, setAppliedValue] = useState(size || \"\");\n\n const [isCustomMode, setIsCustomMode] = useState(false);\n const [customNumberValue, setCustomNumberValue] = useState(\"\");\n const [customUnit, setCustomUnit] = useState(\"px\");\n\n const parseSizeValue = (sizeVal: string) => {\n const numericPart = parseFloat(sizeVal.replace(/[^0-9.]/g, \"\"));\n const unitPart = sizeVal.replace(/[0-9.]/g, \"\");\n return {\n number: isNaN(numericPart) ? \"\" : numericPart.toString(),\n unit: units.includes(unitPart) ? unitPart : \"px\",\n };\n };\n\n useEffect(() => {\n if (isEditingRef.current) return;\n\n if (!size) {\n setDisplayValue(\"\");\n setAppliedValue(\"\");\n setIsCustomMode(true);\n setCustomNumberValue(\"\");\n setCustomUnit(\"px\");\n return;\n }\n\n setDisplayValue(size);\n setAppliedValue(size);\n const { number, unit } = parseSizeValue(size);\n setCustomNumberValue(number);\n setCustomUnit(unit);\n\n const matchingOption = options.find((option) => option.value === size);\n setIsCustomMode(!matchingOption);\n }, [size, options]);\n\n const handleSizeSelect = (value: string) => {\n setDisplayValue(value);\n setAppliedValue(value);\n onChange(value);\n setIsCustomMode(false);\n\n const { number, unit } = parseSizeValue(value);\n setCustomNumberValue(number);\n setCustomUnit(unit);\n };\n\n const handleCustomNumberChange = (e: ChangeEvent<HTMLInputElement>) => {\n e.preventDefault();\n e.stopPropagation();\n\n isEditingRef.current = true;\n\n const numValue = e.target.value;\n setCustomNumberValue(numValue);\n\n const newValue = `${numValue}${customUnit}`;\n setDisplayValue(newValue);\n setIsCustomMode(true);\n };\n\n const handleCustomUnitChange = (e: ChangeEvent<HTMLSelectElement>) => {\n e.preventDefault();\n e.stopPropagation();\n\n isEditingRef.current = true;\n\n const unitValue = e.target.value;\n setCustomUnit(unitValue);\n\n const newValue = `${customNumberValue}${unitValue}`;\n setDisplayValue(newValue);\n setIsCustomMode(true);\n };\n\n const applyCustomSize = () => {\n isEditingRef.current = false;\n setAppliedValue(displayValue);\n onChange(displayValue);\n };\n\n const handleReset = () => {\n isEditingRef.current = false;\n setDisplayValue(\"\");\n setAppliedValue(\"\");\n setCustomNumberValue(\"\");\n setCustomUnit(\"px\");\n onChange(\"\");\n };\n\n return (\n <div\n {...containerProps}\n style={{\n padding: \"8px\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"8px\",\n }}\n >\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"1fr 1fr\",\n gap: \"12px\",\n maxHeight: scroll && options.length > 4 ? \"64px\" : \"none\",\n overflowY: scroll && options.length > 4 ? \"auto\" : \"visible\",\n paddingRight: scroll && options.length > 4 ? \"8px\" : \"0\",\n }}\n >\n {options.map((option, index) => (\n <button\n key={`${option.value}-${index}`}\n className=\"btn btn--icon-style-without-border btn--size-small btn--withoutPopup btn--style-pill btn--withoutPopup\"\n style={{\n cursor: \"pointer\",\n margin: \"0\",\n border:\n appliedValue === option.value && !isCustomMode\n ? \"1px solid var(--theme-elevation-900)\"\n : \"1px solid transparent\",\n }}\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n handleSizeSelect(option.value);\n }}\n >\n {option.label}\n </button>\n ))}\n </div>\n\n {customSize && (\n <div style={{ display: \"flex\", alignItems: \"center\" }}>\n <div style={{ marginRight: \"8px\" }}>Custom: </div>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n width: \"140px\",\n }}\n >\n <div\n className=\"field-type number\"\n onClick={(e) => {\n e.stopPropagation();\n }}\n style={{ flex: 1 }}\n >\n <input\n style={{\n width: \"100%\",\n margin: \"8px 0\",\n borderRight: \"0\",\n height: \"25px\",\n borderTopRightRadius: \"0\",\n borderBottomRightRadius: \"0\",\n paddingTop: \"0\",\n paddingBottom: \"1px\",\n paddingLeft: \"4px\",\n paddingRight: \"4px\",\n }}\n type=\"number\"\n min={1}\n max={999}\n value={customNumberValue}\n onChange={handleCustomNumberChange}\n onClick={(e) => e.stopPropagation()}\n {...inputProps}\n />\n </div>\n <select\n value={customUnit}\n onChange={handleCustomUnitChange}\n onClick={(e) => e.stopPropagation()}\n style={{\n paddingLeft: \"4px\",\n paddingRight: \"4px\",\n width: \"56px\",\n boxShadow: \"0 2px 2px -1px #0000001a\",\n fontFamily: \"var(--font-body)\",\n border: \"1px solid var(--theme-elevation-150)\",\n borderRadius: \"var(--style-radius-s)\",\n background: \"var(--theme-input-bg)\",\n color: \"var(--theme-elevation-800)\",\n fontSize: \"1rem\",\n height: \"25px\",\n lineHeight: \"20px\",\n transitionProperty: \"border, box-shadow, background-color\",\n transitionDuration: \".1s, .1s, .5s\",\n transitionTimingFunction: \"cubic-bezier(0,.2,.2,1)\",\n borderLeft: \"0\",\n transform: \"translateX(-1px)\",\n borderTopLeftRadius: \"0\",\n borderBottomLeftRadius: \"0\",\n outline: \"none\",\n }}\n >\n {units.map((unit, index) => (\n <option key={`${unit}-${index}`} value={unit}>\n {unit}\n </option>\n ))}\n </select>\n </div>\n </div>\n )}\n <div style={{ display: \"flex\", gap: \"8px\" }}>\n <button\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n handleReset();\n }}\n className=\"btn btn--icon-style-without-border btn--size-small btn--withoutPopup btn--style-pill btn--withoutPopup\"\n style={{ marginLeft: \"auto\", margin: \"0\", cursor: \"pointer\", flex: 1 }}\n >\n Reset\n </button>\n {customSize && (\n <button\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n applyCustomSize();\n }}\n className=\"btn btn--icon-style-without-border btn--size-small btn--withoutPopup btn--style-pill btn--withoutPopup\"\n style={{ marginLeft: \"auto\", margin: \"0\", cursor: \"pointer\", flex: 1 }}\n >\n Apply\n </button>\n )}\n </div>\n\n {!hideAttribution && (\n <p\n style={{\n color: \"var(--theme-elevation-650)\",\n fontSize: \"10px\",\n textAlign: \"center\",\n }}\n >\n Made with ❤️ by{\" \"}\n <a target=\"_blank\" href=\"https://github.com/AdrianMaj\">\n @AdrianMaj\n </a>\n </p>\n )}\n </div>\n );\n};\n","import { COMMAND_PRIORITY_CRITICAL } from \"@payloadcms/richtext-lexical/lexical\";\nimport { useLexicalComposerContext } from \"@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext\";\nimport { $patchStyleText } from \"@payloadcms/richtext-lexical/lexical/selection\";\n\nimport { useEffect } from \"react\";\n\nimport { getSelection } from \"../../../utils/getSelection\";\nimport { TEXT_SIZE_COMMAND } from \"../command\";\n\nexport const TextSizeIcon = () => {\n const [editor] = useLexicalComposerContext();\n\n useEffect(() => {\n return editor.registerCommand(\n TEXT_SIZE_COMMAND,\n (payload) => {\n editor.update(() => {\n const selection = getSelection();\n if (selection) $patchStyleText(selection, { size: payload.size || \"\" });\n });\n return false;\n },\n COMMAND_PRIORITY_CRITICAL,\n );\n }, [editor]);\n\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M21 14h-5\" />\n <path d=\"M16 16v-3.5a2.5 2.5 0 0 1 5 0V16\" />\n <path d=\"M4.5 13h6\" />\n <path d=\"m3 16 4.5-9 4.5 9\" />\n </svg>\n );\n};\n","\"use client\";\n\nimport { type ToolbarGroup, type ToolbarGroupItem } from \"@payloadcms/richtext-lexical\";\nimport { createClientFeature } from \"@payloadcms/richtext-lexical/client\";\nimport { COMMAND_PRIORITY_CRITICAL, type BaseSelection } from \"@payloadcms/richtext-lexical/lexical\";\nimport { useLexicalComposerContext } from \"@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext\";\nimport {\n $getSelectionStyleValueForProperty,\n $patchStyleText,\n} from \"@payloadcms/richtext-lexical/lexical/selection\";\n\nimport { useEffect } from \"react\";\n\nimport { TEXT_LETTER_SPACING_COMMAND } from \"./command\";\nimport { Dropdown } from \"./components/TextLetterSpacingDropdown\";\nimport { TextLetterSpacingIcon } from \"./components/TextLetterSpacingIcon\";\n\nimport { getSelection } from \"../../utils/getSelection\";\n\nexport type TextLetterSpacingFeatureProps = {\n hideAttribution?: boolean;\n spacings?: { value: string; label: string }[];\n method?: \"replace\" | \"combine\";\n customSpacing?: boolean;\n scroll?: boolean;\n};\n\nexport type TextLetterSpacingItem = ToolbarGroupItem & {\n command: Record<string, unknown>;\n current: () => string | null;\n} & TextLetterSpacingFeatureProps;\n\nexport const TextLetterSpacingClientFeature = createClientFeature<TextLetterSpacingFeatureProps, TextLetterSpacingItem>(({ props }) => {\n const DropdownComponent: ToolbarGroup = {\n type: \"dropdown\",\n ChildComponent: TextLetterSpacingIcon,\n isEnabled({ selection }: { selection: BaseSelection }) {\n return !!getSelection(selection);\n },\n items: [\n {\n Component: () => {\n const [editor] = useLexicalComposerContext();\n return Dropdown({\n editor,\n item: {\n command: TEXT_LETTER_SPACING_COMMAND,\n current() {\n const selection = getSelection();\n return selection ? $getSelectionStyleValueForProperty(selection, \"letter-spacing\", \"\") : null;\n },\n hideAttribution: props?.hideAttribution,\n spacings: props?.spacings,\n method: props?.method,\n scroll: props?.scroll,\n customSpacing: props?.customSpacing,\n key: \"textLetterSpacing\",\n },\n });\n },\n key: \"textLetterSpacing\",\n },\n ],\n key: \"textLetterSpacingDropdown\",\n order: 62,\n };\n\n return {\n plugins: [\n {\n Component: () => {\n const [editor] = useLexicalComposerContext();\n\n useEffect(() => {\n return editor.registerCommand(\n TEXT_LETTER_SPACING_COMMAND,\n (payload) => {\n editor.update(() => {\n const selection = getSelection();\n if (selection) {\n $patchStyleText(selection, { \"letter-spacing\": payload.spacing || \"\" });\n }\n });\n return true;\n },\n COMMAND_PRIORITY_CRITICAL,\n );\n }, [editor]);\n\n return null;\n },\n position: \"normal\",\n },\n ],\n toolbarFixed: {\n groups: [DropdownComponent],\n },\n toolbarInline: {\n groups: [DropdownComponent],\n },\n };\n});","import { createCommand } from \"@payloadcms/richtext-lexical/lexical\";\n\nexport const TEXT_LETTER_SPACING_COMMAND = createCommand<{ spacing: string }>(\"TEXT_LETTER_SPACING_COMMAND\");","import { type LexicalEditor } from \"@payloadcms/richtext-lexical/lexical\";\n\nimport { useEffect, useState } from \"react\";\n\nimport { SpacingPicker } from \"./TextLetterSpacingPicker\";\n\nimport { type TextLetterSpacingItem } from \"../feature.client\";\n\nexport const Dropdown = ({ editor, item }: { editor: LexicalEditor; item: TextLetterSpacingItem }) => {\n const [activeSpacing, setActiveSpacing] = useState<string>(\"\");\n\n const onChange = (spacing: string) => {\n editor.dispatchCommand(item.command, { spacing });\n setActiveSpacing(spacing || \"\");\n };\n\n useEffect(() => {\n editor.read(() => {\n const current = item.current ? item.current() : null;\n if (current) setActiveSpacing(current);\n });\n }, [editor, item]);\n\n return (\n <SpacingPicker\n spacing={activeSpacing}\n onChange={onChange}\n hideAttribution={item.hideAttribution}\n method={item.method}\n scroll={item.scroll}\n spacings={item.spacings}\n customSpacing={item.customSpacing}\n />\n );\n};","import { useState, useEffect, useRef, type ChangeEvent } from \"react\";\n\nimport { usePreventInlineToolbarClose } from \"../../../utils/usePreventInlineToolbarClose\";\nimport { type TextLetterSpacingFeatureProps } from \"../feature.client\";\n\nexport const SpacingPicker = ({\n spacing,\n onChange,\n hideAttribution,\n spacings,\n method = \"replace\",\n scroll = true,\n customSpacing = true,\n}: {\n spacing: string;\n onChange: (spacing: string) => void;\n} & TextLetterSpacingFeatureProps) => {\n const { containerProps, inputProps } = usePreventInlineToolbarClose();\n const isEditingRef = useRef(false);\n\n // Tailwind spacing values\n const defaultSpacingOptions = [\n { value: \"-0.05em\", label: \"Tighter\" },\n { value: \"-0.025em\", label: \"Tight\" },\n { value: \"0em\", label: \"Normal\" },\n { value: \"0.025em\", label: \"Wide\" },\n { value: \"0.05em\", label: \"Wider\" },\n { value: \"0.1em\", label: \"Widest\" },\n ];\n\n const options =\n method === \"replace\"\n ? (spacings ?? defaultSpacingOptions)\n : [...defaultSpacingOptions, ...(spacings ?? [])];\n\n const units = [\"px\", \"rem\", \"em\", \"%\"];\n\n const [displayValue, setDisplayValue] = useState(spacing || \"\");\n const [appliedValue, setAppliedValue] = useState(spacing || \"\");\n\n const [isCustomMode, setIsCustomMode] = useState(false);\n const [customNumberValue, setCustomNumberValue] = useState(\"\");\n const [customUnit, setCustomUnit] = useState(\"em\");\n\n const parseSpacingValue = (spacingVal: string) => {\n const sign = spacingVal.startsWith(\"-\") ? \"-\" : \"\";\n const cleanedVal = spacingVal.replace(/^-?/, \"\").replace(/[^0-9.]/g, \"\");\n const numericPart = parseFloat(sign + cleanedVal);\n const unitPart = spacingVal.replace(/^-?[0-9.]/g, \"\");\n\n return {\n number: isNaN(numericPart) ? \"\" : numericPart.toString(),\n unit: units.includes(unitPart) ? unitPart : \"em\",\n };\n };\n\n useEffect(() => {\n if (isEditingRef.current) return;\n\n if (!spacing) {\n setDisplayValue(\"\");\n setAppliedValue(\"\");\n setIsCustomMode(true);\n setCustomNumberValue(\"\");\n setCustomUnit(\"em\");\n return;\n }\n\n setDisplayValue(spacing);\n setAppliedValue(spacing);\n const { number, unit } = parseSpacingValue(spacing);\n setCustomNumberValue(number);\n setCustomUnit(unit);\n\n const matchingOption = options.find((option) => option.value === spacing);\n setIsCustomMode(!matchingOption);\n }, [spacing, options]);\n\n const handleSpacingSelect = (value: string) => {\n setDisplayValue(value);\n setAppliedValue(value);\n onChange(value);\n setIsCustomMode(false);\n\n const { number, unit } = parseSpacingValue(value);\n setCustomNumberValue(number);\n setCustomUnit(unit);\n };\n\n const handleCustomNumberChange = (e: ChangeEvent<HTMLInputElement>) => {\n e.preventDefault();\n e.stopPropagation();\n\n isEditingRef.current = true;\n\n const numValue = e.target.value;\n setCustomNumberValue(numValue);\n\n const newValue = `${numValue}${customUnit}`;\n setDisplayValue(newValue);\n setIsCustomMode(true);\n };\n\n const handleCustomUnitChange = (e: ChangeEvent<HTMLSelectElement>) => {\n e.preventDefault();\n e.stopPropagation();\n\n isEditingRef.current = true;\n\n const unitValue = e.target.value;\n setCustomUnit(unitValue);\n\n const newValue = `${customNumberValue}${unitValue}`;\n setDisplayValue(newValue);\n setIsCustomMode(true);\n };\n\n const applyCustomSpacing = () => {\n isEditingRef.current = false;\n setAppliedValue(displayValue);\n onChange(displayValue);\n };\n\n const handleReset = () => {\n isEditingRef.current = false;\n setDisplayValue(\"\");\n setAppliedValue(\"\");\n setCustomNumberValue(\"\");\n setCustomUnit(\"em\");\n onChange(\"\");\n };\n\n return (\n <div\n {...containerProps}\n style={{\n padding: \"8px\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"8px\",\n }}\n >\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"1fr 1fr\",\n gap: \"12px\",\n maxHeight: scroll && options.length > 4 ? \"64px\" : \"none\",\n overflowY: scroll && options.length > 4 ? \"auto\" : \"visible\",\n paddingRight: scroll && options.length > 4 ? \"8px\" : \"0\",\n }}\n >\n {options.map((option, index) => (\n <button\n key={`${option.value}-${index}`}\n className=\"btn btn--icon-style-without-border btn--size-small btn--withoutPopup btn--style-pill btn--withoutPopup\"\n style={{\n cursor: \"pointer\",\n margin: \"0\",\n border:\n appliedValue === option.value && !isCustomMode\n ? \"1px solid var(--theme-elevation-900)\"\n : \"1px solid transparent\",\n }}\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n handleSpacingSelect(option.value);\n }}\n >\n {option.label}\n </button>\n ))}\n </div>\n\n {customSpacing && (\n <div style={{ display: \"flex\", alignItems: \"center\" }}>\n <div style={{ marginRight: \"8px\" }}>Custom: </div>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n width: \"140px\",\n }}\n >\n <div\n className=\"field-type number\"\n onClick={(e) => {\n e.stopPropagation();\n }}\n style={{ flex: 1 }}\n >\n <input\n style={{\n width: \"100%\",\n margin: \"8px 0\",\n borderRight: \"0\",\n height: \"25px\",\n borderTopRightRadius: \"0\",\n borderBottomRightRadius: \"0\",\n paddingTop: \"0\",\n paddingBottom: \"1px\",\n paddingLeft: \"4px\",\n paddingRight: \"4px\",\n }}\n type=\"number\"\n min={0}\n step={0.01}\n max={10}\n value={customNumberValue}\n onChange={handleCustomNumberChange}\n onClick={(e) => e.stopPropagation()}\n {...inputProps}\n />\n </div>\n <select\n value={customUnit}\n onChange={handleCustomUnitChange}\n onClick={(e) => e.stopPropagation()}\n style={{\n paddingLeft: \"4px\",\n paddingRight: \"4px\",\n width: \"56px\",\n boxShadow: \"0 2px 2px -1px #0000001a\",\n fontFamily: \"var(--font-body)\",\n border: \"1px solid var(--theme-elevation-150)\",\n borderRadius: \"var(--style-radius-s)\",\n background: \"var(--theme-input-bg)\",\n color: \"var(--theme-elevation-800)\",\n fontSize: \"1rem\",\n height: \"25px\",\n lineHeight: \"20px\",\n transitionProperty: \"border, box-shadow, background-color\",\n transitionDuration: \".1s, .1s, .5s\",\n transitionTimingFunction: \"cubic-bezier(0,.2,.2,1)\",\n borderLeft: \"0\",\n transform: \"translateX(-1px)\",\n borderTopLeftRadius: \"0\",\n borderBottomLeftRadius: \"0\",\n outline: \"none\",\n }}\n >\n {units.map((unit, index) => (\n <option key={`${unit}-${index}`} value={unit}>\n {unit}\n </option>\n ))}\n </select>\n </div>\n </div>\n )}\n <div style={{ display: \"flex\", gap: \"8px\" }}>\n <button\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n handleReset();\n }}\n className=\"btn btn--icon-style-without-border btn--size-small btn--withoutPopup btn--style-pill btn--withoutPopup\"\n style={{ marginLeft: \"auto\", margin: \"0\", cursor: \"pointer\", flex: 1 }}\n >\n Reset\n </button>\n {customSpacing && (\n <button\n onClick={(e) => {\n e.preventDefault();\n e.stopPropagation();\n applyCustomSpacing();\n }}\n className=\"btn btn--icon-style-without-border btn--size-small btn--withoutPopup btn--style-pill btn--withoutPopup\"\n style={{ marginLeft: \"auto\", margin: \"0\", cursor: \"pointer\", flex: 1 }}\n >\n Apply\n </button>\n )}\n </div>\n\n {!hideAttribution && (\n <p\n style={{\n color: \"var(--theme-elevation-650)\",\n fontSize: \"10px\",\n textAlign: \"center\",\n }}\n >\n Made with ❤️ by{\" \"}\n <a target=\"_blank\" href=\"https://github.com/AdrianMaj\">\n @AdrianMaj\n </a>\n </p>\n )}\n </div>\n );\n};\n","import { COMMAND_PRIORITY_CRITICAL } from \"@payloadcms/richtext-lexical/lexical\";\nimport { useLexicalComposerContext } from \"@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext\";\nimport { $patchStyleText } from \"@payloadcms/richtext-lexical/lexical/selection\";\n\nimport { useEffect } from \"react\";\n\nimport { getSelection } from \"../../../utils/getSelection\";\nimport { TEXT_LETTER_SPACING_COMMAND } from \"../command\";\n\nexport const TextLetterSpacingIcon = () => {\n const [editor] = useLexicalComposerContext();\n\n useEffect(() => {\n return editor.registerCommand(\n TEXT_LETTER_SPACING_COMMAND,\n (payload) => {\n editor.update(() => {\n const selection = getSelection();\n if (selection) $patchStyleText(selection, { \"letter-spacing\": payload.spacing || \"\" });\n });\n return false;\n },\n COMMAND_PRIORITY_CRITICAL,\n );\n }, [editor]);\n\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M2 18h2\" />\n <path d=\"M20 18h2\" />\n <path d=\"M4 7v11\" />\n <path d=\"M20 7v11\" />\n <path d=\"M12 20v2\" />\n <path d=\"M12 14v2\" />\n <path d=\"M12 8v2\" />\n <path d=\"M12 2v2\" />\n </svg>\n );\n};","\"use client\";\n\nimport { type ToolbarGroup, type ToolbarGroupItem } from \"@payloadcms/ri