@aglyn/aglyn-markdown-editor 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +34 -0
- package/package.json +40 -0
- package/src/index.d.ts +43 -0
- package/src/index.js +42 -0
- package/src/index.js.map +1 -0
- package/src/lib/link-target-autocomplete.component.d.ts +60 -0
- package/src/lib/link-target-autocomplete.component.js +235 -0
- package/src/lib/link-target-autocomplete.component.js.map +1 -0
- package/src/lib/markdown-editor-toolbar.component.d.ts +14 -0
- package/src/lib/markdown-editor-toolbar.component.js +254 -0
- package/src/lib/markdown-editor-toolbar.component.js.map +1 -0
- package/src/lib/markdown-field.component.d.ts +43 -0
- package/src/lib/markdown-field.component.js +144 -0
- package/src/lib/markdown-field.component.js.map +1 -0
- package/src/lib/markdown-html-paste.d.ts +38 -0
- package/src/lib/markdown-html-paste.js +312 -0
- package/src/lib/markdown-html-paste.js.map +1 -0
- package/src/lib/markdown-link-dialog.component.d.ts +39 -0
- package/src/lib/markdown-link-dialog.component.js +195 -0
- package/src/lib/markdown-link-dialog.component.js.map +1 -0
- package/src/lib/markdown-lite-view.component.d.ts +6 -0
- package/src/lib/markdown-lite-view.component.js +243 -0
- package/src/lib/markdown-lite-view.component.js.map +1 -0
- package/src/lib/markdown-source-command.d.ts +51 -0
- package/src/lib/markdown-source-command.js +56 -0
- package/src/lib/markdown-source-command.js.map +1 -0
- package/src/lib/markdown-visual-editor.component.d.ts +109 -0
- package/src/lib/markdown-visual-editor.component.js +2019 -0
- package/src/lib/markdown-visual-editor.component.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../libs/aglyn-markdown-editor/src/lib/link-target-autocomplete.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * The link-target lookup (AGL-3119): one searchable control for every place\n * an author picks where a link goes — the designer's Screen pickers, a\n * `Link`-typed component prop, and the markdown editor's link dialog.\n *\n * It lives in this package because this is the one core UI package both the\n * designer and the console's content editor already reach, and the markdown\n * link dialog beside it is one of its users. What it offers comes from two\n * places, and only the second costs a read:\n *\n * - `targets` — the host's screens, collection listings and feeds, which the\n * caller already holds (`screenLinkTargetOptions`). Filtered in memory as\n * the author types.\n * - entries, from `LinkTargetSearchContext`, fetched as the author types and\n * only while the list is open. With no provider mounted there is no\n * Entries group at all, and everything else works as before.\n *\n * The value is the routing-map KEY of the chosen target (a bare screen id,\n * `collection:`, `feed:`, `entry:`) or one of the caller's pinned choices.\n * How a key is stored is the caller's business: a Screen slot keeps it bare,\n * a `Link` prop and a markdown link wrap a screen id as `screen:<id>`.\n */\n\nimport type * as Aglyn from '@aglyn/aglyn'\nimport {\n SAFE_HREF_PATTERN,\n linkTargetKind,\n useLinkTargetEntrySearch,\n useLinkTargetLabel,\n} from '@aglyn/aglyn'\nimport { Autocomplete, Box, ListSubheader, TextField } from '@mui/material'\nimport { useMemo, useState, type ReactNode } from 'react'\n\n/**\n * Value standing for \"type a URL instead\" (AGL-1335).\n *\n * UI-only: it is never stored. The persisted value in that mode is the URL\n * the author types, so the AGL-1191 rule about `''`-valued options does not\n * bite here — the one `''` option means genuinely unset, which is exactly\n * what the graft reads as \"fall back to the component's default\".\n *\n * Spelled with a character no Firestore id contains so it can never collide\n * with a real screen id in the same list.\n */\nexport const EXTERNAL_URL_OPTION = 'external-url:'\n\n/** A choice pinned above or below the targets, never filtered away. */\nexport interface LinkTargetChoice {\n value: string\n label: string\n}\n\n/** What a choice carries beyond its value. */\nexport interface LinkTargetChangeDetail {\n /** The address the author typed, when they picked it as typed. */\n address?: string\n /** An entry's own title, for link text that should read as a sentence. */\n title?: string\n}\n\nexport interface LinkTargetAutocompleteProps {\n /**\n * The chosen target's key or pinned choice's value; `null` for nothing\n * chosen yet.\n */\n value: string | null | undefined\n onChange: (value: string, detail: LinkTargetChangeDetail) => void\n /** Screens, listings and feeds, in the order they are offered. */\n targets: readonly LinkTargetChoice[]\n /** Pinned first: the empty choice, a stored target the host has lost. */\n leading?: readonly LinkTargetChoice[]\n /** Pinned last: the external-address mode. */\n trailing?: readonly LinkTargetChoice[]\n /**\n * The trailing choice that stands for a typed address. When what the\n * author typed already looks like one (`https://…`, `/pricing`), that\n * choice offers to use it as typed.\n */\n addressOption?: string\n label?: ReactNode\n helperText?: ReactNode\n placeholder?: string\n name?: string\n size?: 'small' | 'medium'\n disabled?: boolean\n error?: boolean\n autoFocus?: boolean\n /** Open the list on focus — for a dialog whose first job is choosing. */\n openOnFocus?: boolean\n}\n\ninterface PickerOption {\n key: string\n value: string\n label: string\n group: string\n /** Shown whatever is typed. */\n pinned?: boolean\n disabled?: boolean\n address?: string\n title?: string\n}\n\nconst GROUP_BY_KIND: Readonly<Record<Aglyn.LinkTargetKind, string>> = {\n screen: 'Pages',\n collection: 'Collection listings',\n feed: 'RSS feeds',\n entry: 'Entries',\n}\n\nconst ENTRIES_GROUP = GROUP_BY_KIND.entry\n\n/**\n * The value of a row that only reports on the search; never selectable.\n *\n * Written as an escape, not as the character itself: a raw NUL byte in the\n * source makes git read the whole file as binary, so it has no diff and no\n * line-wise merge.\n */\nconst STATUS_VALUE = '\\u0000status'\n\n/** The typed text, when it already looks like a link address. */\nfunction typedAddress(query: string): string | undefined {\n const trimmed = query.trim()\n return trimmed && SAFE_HREF_PATTERN.test(trimmed) ? trimmed : undefined\n}\n\nfunction findChoice(\n value: string,\n ...lists: ReadonlyArray<readonly LinkTargetChoice[] | undefined>\n): LinkTargetChoice | undefined {\n for (const list of lists) {\n const found = list?.find((choice) => choice.value === value)\n if (found) return found\n }\n return undefined\n}\n\n/**\n * Screens, listings and feeds narrow to what was typed; entries arrive\n * already narrowed by the search, and pinned rows stay put.\n */\nfunction filterPickerOptions(\n options: PickerOption[],\n state: { inputValue: string },\n): PickerOption[] {\n const needle = state.inputValue.trim().toLowerCase()\n if (!needle) return options\n return options.filter(\n (option) =>\n option.pinned ||\n option.group === ENTRIES_GROUP ||\n option.label.toLowerCase().includes(needle),\n )\n}\n\nexport function LinkTargetAutocomplete(props: LinkTargetAutocompleteProps) {\n const {\n value,\n onChange,\n targets,\n leading,\n trailing,\n addressOption,\n label,\n helperText,\n placeholder,\n name,\n size = 'small',\n disabled,\n error,\n autoFocus,\n openOnFocus,\n } = props\n const [open, setOpen] = useState(false)\n // What the author TYPED, as distinct from the input's text: once a target\n // is chosen the input shows its label, and searching for that label would\n // spend a read to find what is already selected.\n const [query, setQuery] = useState('')\n const search = useLinkTargetEntrySearch(query, open && !disabled)\n\n const chosen = typeof value === 'string' ? value : undefined\n const known =\n chosen === undefined ? undefined : findChoice(chosen, leading, targets, trailing)\n // A chosen value no list holds — an entry, whose name only the search seam\n // can read.\n const described = useLinkTargetLabel(\n chosen !== undefined && !known ? chosen : undefined,\n )\n const selectedLabel = known?.label ?? described?.label\n\n // Keyed on the value and its label only. The input re-syncs its text\n // whenever this object changes, so rebuilding it when unrelated results\n // arrive would overwrite what the author is typing.\n const selected = useMemo<PickerOption | null>(\n () =>\n chosen === undefined || selectedLabel === undefined\n ? null\n : {\n key: `selected:${chosen}`,\n value: chosen,\n label: selectedLabel,\n group: '',\n pinned: true,\n },\n [chosen, selectedLabel],\n )\n\n const address = addressOption ? typedAddress(query) : undefined\n const options = useMemo(() => {\n const list: PickerOption[] = []\n for (const choice of leading ?? []) {\n list.push({\n key: `leading:${choice.value}`,\n value: choice.value,\n label: choice.label,\n group: '',\n pinned: true,\n })\n }\n // The current choice stays on offer while the search holds no row for\n // it, or the list would not contain the value the input shows.\n if (\n selected &&\n !known &&\n !search.entries.some((entry) => entry.value === selected.value)\n ) {\n list.push(selected)\n }\n for (const target of targets) {\n list.push({\n key: `target:${target.value}`,\n value: target.value,\n label: target.label,\n group: GROUP_BY_KIND[linkTargetKind(target.value)],\n })\n }\n if (search.available) {\n for (const entry of search.entries) {\n list.push({\n key: `entry:${entry.value}`,\n value: entry.value,\n label: entry.label,\n group: ENTRIES_GROUP,\n title: entry.title,\n })\n }\n const status = search.searching\n ? 'Searching entries…'\n : search.failed\n ? 'Entries could not be searched. Try again.'\n : search.entries.length\n ? undefined\n : query.trim()\n ? 'No entry address starts with that'\n : 'No entries yet'\n if (status) {\n list.push({\n key: 'entries-status',\n value: STATUS_VALUE,\n label: status,\n group: ENTRIES_GROUP,\n pinned: true,\n disabled: true,\n })\n }\n }\n for (const choice of trailing ?? []) {\n const typed = choice.value === addressOption ? address : undefined\n list.push({\n key: `trailing:${choice.value}`,\n value: choice.value,\n label: typed ? `Use the address ${typed}` : choice.label,\n group: '',\n pinned: true,\n address: typed,\n })\n }\n return list\n }, [\n leading,\n selected,\n known,\n targets,\n search.available,\n search.entries,\n search.searching,\n search.failed,\n query,\n trailing,\n addressOption,\n address,\n ])\n\n return (\n <Autocomplete<PickerOption, false, true, false>\n size={size}\n fullWidth\n disabled={disabled}\n disableClearable\n openOnFocus={openOnFocus}\n open={open}\n onOpen={() => setOpen(true)}\n onClose={() => setOpen(false)}\n options={options}\n // `null` is \"nothing chosen yet\"; the typing only rules it out because\n // `disableClearable` means no control here can set it.\n value={selected as PickerOption}\n getOptionLabel={(option) => option.label}\n getOptionKey={(option) => option.key}\n getOptionDisabled={(option) => Boolean(option.disabled)}\n isOptionEqualToValue={(option, current) => option.value === current.value}\n groupBy={(option) => option.group}\n filterOptions={filterPickerOptions}\n renderGroup={(params) => (\n <li key={params.key}>\n {params.group ? (\n <ListSubheader component=\"div\" sx={{ lineHeight: '32px' }}>\n {params.group}\n </ListSubheader>\n ) : null}\n <Box component=\"ul\" sx={{ p: 0 }}>\n {params.children}\n </Box>\n </li>\n )}\n onInputChange={(_event, text, reason) => {\n setQuery(reason === 'input' ? text : '')\n }}\n onChange={(_event, option) => {\n if (!option || option.disabled) return\n onChange(option.value, {\n ...(option.address ? { address: option.address } : {}),\n ...(option.title ? { title: option.title } : {}),\n })\n }}\n renderInput={(params) => (\n <TextField\n {...params}\n name={name}\n label={label}\n placeholder={placeholder}\n helperText={helperText}\n error={error}\n autoFocus={autoFocus}\n // Shrunk always: a chosen label fills the input, and an empty one\n // shows the placeholder, which a floating label would sit on.\n slotProps={{\n ...params.slotProps,\n inputLabel: { ...params.slotProps.inputLabel, shrink: true },\n }}\n />\n )}\n />\n )\n}\nLinkTargetAutocomplete.displayName = 'LinkTargetAutocomplete'\n\nexport default LinkTargetAutocomplete\n"],"names":["SAFE_HREF_PATTERN","linkTargetKind","useLinkTargetEntrySearch","useLinkTargetLabel","Autocomplete","Box","ListSubheader","TextField","useMemo","useState","EXTERNAL_URL_OPTION","GROUP_BY_KIND","screen","collection","feed","entry","ENTRIES_GROUP","STATUS_VALUE","typedAddress","query","trimmed","trim","test","undefined","findChoice","value","lists","list","found","find","choice","filterPickerOptions","options","state","needle","inputValue","toLowerCase","filter","option","pinned","group","label","includes","LinkTargetAutocomplete","props","onChange","targets","leading","trailing","addressOption","helperText","placeholder","name","size","disabled","error","autoFocus","openOnFocus","open","setOpen","setQuery","search","chosen","known","described","selectedLabel","selected","key","address","push","entries","some","target","available","title","status","searching","failed","length","typed","fullWidth","disableClearable","onOpen","onClose","getOptionLabel","getOptionKey","getOptionDisabled","Boolean","isOptionEqualToValue","current","groupBy","filterOptions","renderGroup","params","li","component","sx","lineHeight","p","children","onInputChange","_event","text","reason","renderInput","slotProps","inputLabel","shrink","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { MarkdownEditorCommand, MarkdownEditorContext } from './markdown-visual-editor.component';
|
|
3
|
+
export interface MarkdownEditorToolbarProps {
|
|
4
|
+
onCommand: (command: MarkdownEditorCommand) => void;
|
|
5
|
+
/** Caret context from the visual editor; absent on the markdown surface. */
|
|
6
|
+
context?: MarkdownEditorContext | null;
|
|
7
|
+
/** Shown when the host page offers both editing surfaces. */
|
|
8
|
+
mode?: 'visual' | 'markdown';
|
|
9
|
+
onModeChange?: (mode: 'visual' | 'markdown') => void;
|
|
10
|
+
/** Extra actions pinned to the right (AI assist). */
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare function MarkdownEditorToolbar({ onCommand, context, mode, onModeChange, children, }: MarkdownEditorToolbarProps): import("react").JSX.Element;
|
|
14
|
+
export default MarkdownEditorToolbar;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
+
/**
|
|
19
|
+
* The one markdown toolbar (AGL-984). Blog bodies and marketplace READMEs
|
|
20
|
+
* are the same dialect edited by the same component, so they get the same
|
|
21
|
+
* control: grouped MUI toggle buttons — marks, block kind, inserts — plus
|
|
22
|
+
* the Visual / Markdown switch when the host page offers both surfaces.
|
|
23
|
+
*
|
|
24
|
+
* `context` is what the visual editor reports about the caret, so Bold on a
|
|
25
|
+
* bold run reads pressed. Every button preventDefaults on mousedown: the
|
|
26
|
+
* editor's DOM selection has to survive the click, or the command would
|
|
27
|
+
* apply to nothing.
|
|
28
|
+
*/ import CodeIcon from "@mui/icons-material/Code";
|
|
29
|
+
import FormatBoldIcon from "@mui/icons-material/FormatBold";
|
|
30
|
+
import FormatItalicIcon from "@mui/icons-material/FormatItalic";
|
|
31
|
+
import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
|
|
32
|
+
import FormatListNumberedIcon from "@mui/icons-material/FormatListNumbered";
|
|
33
|
+
import FormatQuoteIcon from "@mui/icons-material/FormatQuote";
|
|
34
|
+
import ImageIcon from "@mui/icons-material/Image";
|
|
35
|
+
import InsertLinkIcon from "@mui/icons-material/InsertLink";
|
|
36
|
+
import TableChartIcon from "@mui/icons-material/TableChart";
|
|
37
|
+
import TitleIcon from "@mui/icons-material/Title";
|
|
38
|
+
import { Box, Divider, Paper, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from "@mui/material";
|
|
39
|
+
/** Groups sit flush inside one bar, MUI's grouped-toggle customization. */ const groupSx = {
|
|
40
|
+
'& .MuiToggleButtonGroup-grouped': {
|
|
41
|
+
m: 0.5,
|
|
42
|
+
border: 0,
|
|
43
|
+
borderRadius: 1,
|
|
44
|
+
px: 1,
|
|
45
|
+
'&.Mui-disabled': {
|
|
46
|
+
border: 0
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
function ToolbarButton({ command, label, icon, onCommand, selected }) {
|
|
51
|
+
return /*#__PURE__*/ _jsx(Tooltip, {
|
|
52
|
+
title: label,
|
|
53
|
+
children: /*#__PURE__*/ _jsx(ToggleButton, {
|
|
54
|
+
value: command,
|
|
55
|
+
"aria-label": label,
|
|
56
|
+
selected: Boolean(selected),
|
|
57
|
+
// Not onChange: an insert is an action, not a state the group owns.
|
|
58
|
+
onMouseDown: (event)=>event.preventDefault(),
|
|
59
|
+
onClick: ()=>onCommand(command),
|
|
60
|
+
children: icon
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export function MarkdownEditorToolbar({ onCommand, context, mode, onModeChange, children }) {
|
|
65
|
+
const kind = context == null ? void 0 : context.kind;
|
|
66
|
+
return /*#__PURE__*/ _jsxs(Paper, {
|
|
67
|
+
elevation: 0,
|
|
68
|
+
variant: "outlined",
|
|
69
|
+
sx: {
|
|
70
|
+
display: 'flex',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
flexWrap: 'wrap',
|
|
73
|
+
borderRadius: 1
|
|
74
|
+
},
|
|
75
|
+
children: [
|
|
76
|
+
/*#__PURE__*/ _jsxs(ToggleButtonGroup, {
|
|
77
|
+
size: "small",
|
|
78
|
+
sx: groupSx,
|
|
79
|
+
children: [
|
|
80
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
81
|
+
command: "bold",
|
|
82
|
+
label: "Bold",
|
|
83
|
+
icon: /*#__PURE__*/ _jsx(FormatBoldIcon, {
|
|
84
|
+
fontSize: "small"
|
|
85
|
+
}),
|
|
86
|
+
onCommand: onCommand,
|
|
87
|
+
selected: context == null ? void 0 : context.bold
|
|
88
|
+
}),
|
|
89
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
90
|
+
command: "italic",
|
|
91
|
+
label: "Italic",
|
|
92
|
+
icon: /*#__PURE__*/ _jsx(FormatItalicIcon, {
|
|
93
|
+
fontSize: "small"
|
|
94
|
+
}),
|
|
95
|
+
onCommand: onCommand,
|
|
96
|
+
selected: context == null ? void 0 : context.italic
|
|
97
|
+
})
|
|
98
|
+
]
|
|
99
|
+
}),
|
|
100
|
+
/*#__PURE__*/ _jsx(Divider, {
|
|
101
|
+
flexItem: true,
|
|
102
|
+
orientation: "vertical",
|
|
103
|
+
sx: {
|
|
104
|
+
my: 1
|
|
105
|
+
}
|
|
106
|
+
}),
|
|
107
|
+
/*#__PURE__*/ _jsxs(ToggleButtonGroup, {
|
|
108
|
+
size: "small",
|
|
109
|
+
sx: groupSx,
|
|
110
|
+
children: [
|
|
111
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
112
|
+
command: "heading",
|
|
113
|
+
label: "Heading",
|
|
114
|
+
icon: /*#__PURE__*/ _jsx(TitleIcon, {
|
|
115
|
+
fontSize: "small"
|
|
116
|
+
}),
|
|
117
|
+
onCommand: onCommand,
|
|
118
|
+
selected: kind === 'heading2'
|
|
119
|
+
}),
|
|
120
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
121
|
+
command: "heading3",
|
|
122
|
+
label: "Subheading",
|
|
123
|
+
icon: /*#__PURE__*/ _jsx(TitleIcon, {
|
|
124
|
+
sx: {
|
|
125
|
+
fontSize: 16
|
|
126
|
+
}
|
|
127
|
+
}),
|
|
128
|
+
onCommand: onCommand,
|
|
129
|
+
selected: kind === 'heading3'
|
|
130
|
+
}),
|
|
131
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
132
|
+
command: "list",
|
|
133
|
+
label: "Bullet list",
|
|
134
|
+
icon: /*#__PURE__*/ _jsx(FormatListBulletedIcon, {
|
|
135
|
+
fontSize: "small"
|
|
136
|
+
}),
|
|
137
|
+
onCommand: onCommand,
|
|
138
|
+
selected: kind === 'listItem'
|
|
139
|
+
}),
|
|
140
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
141
|
+
command: "orderedList",
|
|
142
|
+
label: "Numbered list",
|
|
143
|
+
icon: /*#__PURE__*/ _jsx(FormatListNumberedIcon, {
|
|
144
|
+
fontSize: "small"
|
|
145
|
+
}),
|
|
146
|
+
onCommand: onCommand,
|
|
147
|
+
selected: kind === 'orderedItem'
|
|
148
|
+
}),
|
|
149
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
150
|
+
command: "quote",
|
|
151
|
+
label: "Quote",
|
|
152
|
+
icon: /*#__PURE__*/ _jsx(FormatQuoteIcon, {
|
|
153
|
+
fontSize: "small"
|
|
154
|
+
}),
|
|
155
|
+
onCommand: onCommand,
|
|
156
|
+
selected: kind === 'quote'
|
|
157
|
+
})
|
|
158
|
+
]
|
|
159
|
+
}),
|
|
160
|
+
/*#__PURE__*/ _jsx(Divider, {
|
|
161
|
+
flexItem: true,
|
|
162
|
+
orientation: "vertical",
|
|
163
|
+
sx: {
|
|
164
|
+
my: 1
|
|
165
|
+
}
|
|
166
|
+
}),
|
|
167
|
+
/*#__PURE__*/ _jsxs(ToggleButtonGroup, {
|
|
168
|
+
size: "small",
|
|
169
|
+
sx: groupSx,
|
|
170
|
+
children: [
|
|
171
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
172
|
+
command: "link",
|
|
173
|
+
label: "Link",
|
|
174
|
+
icon: /*#__PURE__*/ _jsx(InsertLinkIcon, {
|
|
175
|
+
fontSize: "small"
|
|
176
|
+
}),
|
|
177
|
+
onCommand: onCommand
|
|
178
|
+
}),
|
|
179
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
180
|
+
command: "image",
|
|
181
|
+
label: "Image",
|
|
182
|
+
icon: /*#__PURE__*/ _jsx(ImageIcon, {
|
|
183
|
+
fontSize: "small"
|
|
184
|
+
}),
|
|
185
|
+
onCommand: onCommand
|
|
186
|
+
}),
|
|
187
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
188
|
+
command: "code",
|
|
189
|
+
label: "Code block",
|
|
190
|
+
icon: /*#__PURE__*/ _jsx(CodeIcon, {
|
|
191
|
+
fontSize: "small"
|
|
192
|
+
}),
|
|
193
|
+
onCommand: onCommand
|
|
194
|
+
}),
|
|
195
|
+
/*#__PURE__*/ _jsx(ToolbarButton, {
|
|
196
|
+
command: "table",
|
|
197
|
+
label: "Table",
|
|
198
|
+
icon: /*#__PURE__*/ _jsx(TableChartIcon, {
|
|
199
|
+
fontSize: "small"
|
|
200
|
+
}),
|
|
201
|
+
onCommand: onCommand
|
|
202
|
+
})
|
|
203
|
+
]
|
|
204
|
+
}),
|
|
205
|
+
/*#__PURE__*/ _jsx(Box, {
|
|
206
|
+
sx: {
|
|
207
|
+
flexGrow: 1
|
|
208
|
+
}
|
|
209
|
+
}),
|
|
210
|
+
children,
|
|
211
|
+
mode && onModeChange ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
212
|
+
children: [
|
|
213
|
+
/*#__PURE__*/ _jsx(Divider, {
|
|
214
|
+
flexItem: true,
|
|
215
|
+
orientation: "vertical",
|
|
216
|
+
sx: {
|
|
217
|
+
my: 1
|
|
218
|
+
}
|
|
219
|
+
}),
|
|
220
|
+
/*#__PURE__*/ _jsxs(ToggleButtonGroup, {
|
|
221
|
+
exclusive: true,
|
|
222
|
+
size: "small",
|
|
223
|
+
value: mode,
|
|
224
|
+
onChange: (_event, next)=>{
|
|
225
|
+
if (next) onModeChange(next);
|
|
226
|
+
},
|
|
227
|
+
sx: groupSx,
|
|
228
|
+
children: [
|
|
229
|
+
/*#__PURE__*/ _jsx(ToggleButton, {
|
|
230
|
+
value: "visual",
|
|
231
|
+
"aria-label": "Visual editor",
|
|
232
|
+
children: /*#__PURE__*/ _jsx(Typography, {
|
|
233
|
+
variant: "caption",
|
|
234
|
+
children: 'Visual'
|
|
235
|
+
})
|
|
236
|
+
}),
|
|
237
|
+
/*#__PURE__*/ _jsx(ToggleButton, {
|
|
238
|
+
value: "markdown",
|
|
239
|
+
"aria-label": "Markdown source",
|
|
240
|
+
children: /*#__PURE__*/ _jsx(Typography, {
|
|
241
|
+
variant: "caption",
|
|
242
|
+
children: 'Markdown'
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
]
|
|
246
|
+
})
|
|
247
|
+
]
|
|
248
|
+
}) : null
|
|
249
|
+
]
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
export default MarkdownEditorToolbar;
|
|
253
|
+
|
|
254
|
+
//# sourceMappingURL=markdown-editor-toolbar.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../libs/aglyn-markdown-editor/src/lib/markdown-editor-toolbar.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\n/**\n * The one markdown toolbar (AGL-984). Blog bodies and marketplace READMEs\n * are the same dialect edited by the same component, so they get the same\n * control: grouped MUI toggle buttons — marks, block kind, inserts — plus\n * the Visual / Markdown switch when the host page offers both surfaces.\n *\n * `context` is what the visual editor reports about the caret, so Bold on a\n * bold run reads pressed. Every button preventDefaults on mousedown: the\n * editor's DOM selection has to survive the click, or the command would\n * apply to nothing.\n */\n\nimport CodeIcon from '@mui/icons-material/Code'\nimport FormatBoldIcon from '@mui/icons-material/FormatBold'\nimport FormatItalicIcon from '@mui/icons-material/FormatItalic'\nimport FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'\nimport FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'\nimport FormatQuoteIcon from '@mui/icons-material/FormatQuote'\nimport ImageIcon from '@mui/icons-material/Image'\nimport InsertLinkIcon from '@mui/icons-material/InsertLink'\nimport TableChartIcon from '@mui/icons-material/TableChart'\nimport TitleIcon from '@mui/icons-material/Title'\nimport {\n Box,\n Divider,\n Paper,\n ToggleButton,\n ToggleButtonGroup,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport type { ReactNode } from 'react'\nimport type {\n MarkdownEditorCommand,\n MarkdownEditorContext,\n} from './markdown-visual-editor.component'\n\nexport interface MarkdownEditorToolbarProps {\n onCommand: (command: MarkdownEditorCommand) => void\n /** Caret context from the visual editor; absent on the markdown surface. */\n context?: MarkdownEditorContext | null\n /** Shown when the host page offers both editing surfaces. */\n mode?: 'visual' | 'markdown'\n onModeChange?: (mode: 'visual' | 'markdown') => void\n /** Extra actions pinned to the right (AI assist). */\n children?: ReactNode\n}\n\n/** Groups sit flush inside one bar, MUI's grouped-toggle customization. */\nconst groupSx = {\n '& .MuiToggleButtonGroup-grouped': {\n m: 0.5,\n border: 0,\n borderRadius: 1,\n px: 1,\n '&.Mui-disabled': { border: 0 },\n },\n} as const\n\ninterface ToolbarButtonProps {\n command: MarkdownEditorCommand\n label: string\n icon: ReactNode\n onCommand: (command: MarkdownEditorCommand) => void\n selected?: boolean\n}\n\nfunction ToolbarButton({\n command,\n label,\n icon,\n onCommand,\n selected,\n}: ToolbarButtonProps) {\n return (\n <Tooltip title={label}>\n <ToggleButton\n value={command}\n aria-label={label}\n selected={Boolean(selected)}\n // Not onChange: an insert is an action, not a state the group owns.\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => onCommand(command)}\n >\n {icon}\n </ToggleButton>\n </Tooltip>\n )\n}\n\nexport function MarkdownEditorToolbar({\n onCommand,\n context,\n mode,\n onModeChange,\n children,\n}: MarkdownEditorToolbarProps) {\n const kind = context?.kind\n return (\n <Paper\n elevation={0}\n variant=\"outlined\"\n sx={{\n display: 'flex',\n alignItems: 'center',\n flexWrap: 'wrap',\n borderRadius: 1,\n }}\n >\n <ToggleButtonGroup size=\"small\" sx={groupSx}>\n <ToolbarButton\n command=\"bold\"\n label=\"Bold\"\n icon={<FormatBoldIcon fontSize=\"small\" />}\n onCommand={onCommand}\n selected={context?.bold}\n />\n <ToolbarButton\n command=\"italic\"\n label=\"Italic\"\n icon={<FormatItalicIcon fontSize=\"small\" />}\n onCommand={onCommand}\n selected={context?.italic}\n />\n </ToggleButtonGroup>\n\n <Divider flexItem orientation=\"vertical\" sx={{ my: 1 }} />\n\n <ToggleButtonGroup size=\"small\" sx={groupSx}>\n <ToolbarButton\n command=\"heading\"\n label=\"Heading\"\n icon={<TitleIcon fontSize=\"small\" />}\n onCommand={onCommand}\n selected={kind === 'heading2'}\n />\n <ToolbarButton\n command=\"heading3\"\n label=\"Subheading\"\n icon={<TitleIcon sx={{ fontSize: 16 }} />}\n onCommand={onCommand}\n selected={kind === 'heading3'}\n />\n <ToolbarButton\n command=\"list\"\n label=\"Bullet list\"\n icon={<FormatListBulletedIcon fontSize=\"small\" />}\n onCommand={onCommand}\n selected={kind === 'listItem'}\n />\n <ToolbarButton\n command=\"orderedList\"\n label=\"Numbered list\"\n icon={<FormatListNumberedIcon fontSize=\"small\" />}\n onCommand={onCommand}\n selected={kind === 'orderedItem'}\n />\n <ToolbarButton\n command=\"quote\"\n label=\"Quote\"\n icon={<FormatQuoteIcon fontSize=\"small\" />}\n onCommand={onCommand}\n selected={kind === 'quote'}\n />\n </ToggleButtonGroup>\n\n <Divider flexItem orientation=\"vertical\" sx={{ my: 1 }} />\n\n <ToggleButtonGroup size=\"small\" sx={groupSx}>\n <ToolbarButton\n command=\"link\"\n label=\"Link\"\n icon={<InsertLinkIcon fontSize=\"small\" />}\n onCommand={onCommand}\n />\n <ToolbarButton\n command=\"image\"\n label=\"Image\"\n icon={<ImageIcon fontSize=\"small\" />}\n onCommand={onCommand}\n />\n <ToolbarButton\n command=\"code\"\n label=\"Code block\"\n icon={<CodeIcon fontSize=\"small\" />}\n onCommand={onCommand}\n />\n <ToolbarButton\n command=\"table\"\n label=\"Table\"\n icon={<TableChartIcon fontSize=\"small\" />}\n onCommand={onCommand}\n />\n </ToggleButtonGroup>\n\n <Box sx={{ flexGrow: 1 }} />\n\n {children}\n\n {mode && onModeChange ? (\n <>\n <Divider flexItem orientation=\"vertical\" sx={{ my: 1 }} />\n <ToggleButtonGroup\n exclusive\n size=\"small\"\n value={mode}\n onChange={(_event, next) => {\n if (next) onModeChange(next as 'visual' | 'markdown')\n }}\n sx={groupSx}\n >\n <ToggleButton value=\"visual\" aria-label=\"Visual editor\">\n <Typography variant=\"caption\">{'Visual'}</Typography>\n </ToggleButton>\n <ToggleButton value=\"markdown\" aria-label=\"Markdown source\">\n <Typography variant=\"caption\">{'Markdown'}</Typography>\n </ToggleButton>\n </ToggleButtonGroup>\n </>\n ) : null}\n </Paper>\n )\n}\n\nexport default MarkdownEditorToolbar\n"],"names":["CodeIcon","FormatBoldIcon","FormatItalicIcon","FormatListBulletedIcon","FormatListNumberedIcon","FormatQuoteIcon","ImageIcon","InsertLinkIcon","TableChartIcon","TitleIcon","Box","Divider","Paper","ToggleButton","ToggleButtonGroup","Tooltip","Typography","groupSx","m","border","borderRadius","px","ToolbarButton","command","label","icon","onCommand","selected","title","value","aria-label","Boolean","onMouseDown","event","preventDefault","onClick","MarkdownEditorToolbar","context","mode","onModeChange","children","kind","elevation","variant","sx","display","alignItems","flexWrap","size","fontSize","bold","italic","flexItem","orientation","my","flexGrow","exclusive","onChange","_event","next"],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface MarkdownFieldHandle {
|
|
2
|
+
/** Insert an image at the cursor — used by a media picker upstream. */
|
|
3
|
+
insertImage: (alt: string, url: string) => void;
|
|
4
|
+
}
|
|
5
|
+
export interface MarkdownFieldProps {
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
onChange: (value: string) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Open the caller's media picker, with the alt text the author typed in the
|
|
11
|
+
* image dialog (AGL-1645). Omitted when there is nowhere to pick from — the
|
|
12
|
+
* toolbar's image action is then the editor's own prompt.
|
|
13
|
+
*/
|
|
14
|
+
onPickImageFromMedia?: (alt: string) => void;
|
|
15
|
+
minHeight?: number;
|
|
16
|
+
/** Shown under the editor in visual mode. */
|
|
17
|
+
helperText?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A markdown field: toolbar, visual editor, and a raw-source escape hatch
|
|
21
|
+
* (AGL-1080).
|
|
22
|
+
*
|
|
23
|
+
* This plumbing — one toolbar driving two surfaces, a mode switch, a
|
|
24
|
+
* source-mode command path that wraps the textarea selection, and a handle
|
|
25
|
+
* for inserting picked media — lived only inside the listing detail editor.
|
|
26
|
+
* The publish form, which is where a README is written for the FIRST time,
|
|
27
|
+
* had a four-row textarea instead: the worst tool on the first pass and the
|
|
28
|
+
* good one on the second, for the same field.
|
|
29
|
+
*
|
|
30
|
+
* Copying sixty lines into the second caller is how two surfaces for one
|
|
31
|
+
* piece of content drift apart, which is the complaint this closes. So it
|
|
32
|
+
* is extracted once and used twice.
|
|
33
|
+
*
|
|
34
|
+
* The media picker itself stays with the caller: it needs a scope (an org
|
|
35
|
+
* or a host) that this component has no business knowing.
|
|
36
|
+
*/
|
|
37
|
+
export declare function MarkdownField(props: MarkdownFieldProps & {
|
|
38
|
+
editorRef?: (handle: MarkdownFieldHandle | null) => void;
|
|
39
|
+
}): import("react").JSX.Element;
|
|
40
|
+
export declare namespace MarkdownField {
|
|
41
|
+
var displayName: string;
|
|
42
|
+
}
|
|
43
|
+
export default MarkdownField;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { Stack, TextField, Typography } from "@mui/material";
|
|
19
|
+
import { useRef, useState } from "react";
|
|
20
|
+
import MarkdownEditorToolbar from "./markdown-editor-toolbar.component.js";
|
|
21
|
+
import MarkdownLinkDialog from "./markdown-link-dialog.component.js";
|
|
22
|
+
import { applyCommandToSource, applyLinkToSource, MARKDOWN_SOURCE_HINT } from "./markdown-source-command.js";
|
|
23
|
+
import MarkdownVisualEditor from "./markdown-visual-editor.component.js";
|
|
24
|
+
/**
|
|
25
|
+
* A markdown field: toolbar, visual editor, and a raw-source escape hatch
|
|
26
|
+
* (AGL-1080).
|
|
27
|
+
*
|
|
28
|
+
* This plumbing — one toolbar driving two surfaces, a mode switch, a
|
|
29
|
+
* source-mode command path that wraps the textarea selection, and a handle
|
|
30
|
+
* for inserting picked media — lived only inside the listing detail editor.
|
|
31
|
+
* The publish form, which is where a README is written for the FIRST time,
|
|
32
|
+
* had a four-row textarea instead: the worst tool on the first pass and the
|
|
33
|
+
* good one on the second, for the same field.
|
|
34
|
+
*
|
|
35
|
+
* Copying sixty lines into the second caller is how two surfaces for one
|
|
36
|
+
* piece of content drift apart, which is the complaint this closes. So it
|
|
37
|
+
* is extracted once and used twice.
|
|
38
|
+
*
|
|
39
|
+
* The media picker itself stays with the caller: it needs a scope (an org
|
|
40
|
+
* or a host) that this component has no business knowing.
|
|
41
|
+
*/ export function MarkdownField(props) {
|
|
42
|
+
const { label, value, onChange, onPickImageFromMedia, minHeight = 200, helperText, editorRef } = props;
|
|
43
|
+
const visualRef = useRef(null);
|
|
44
|
+
const sourceRef = useRef(null);
|
|
45
|
+
// Visual by default, raw markdown one click away (AGL-985) — publishers
|
|
46
|
+
// often arrive holding a README they wrote somewhere else.
|
|
47
|
+
const [mode, setMode] = useState('visual');
|
|
48
|
+
const [context, setContext] = useState(null);
|
|
49
|
+
/** The source-mode selection a link is being written over (AGL-3119). */ const [sourceLink, setSourceLink] = useState(null);
|
|
50
|
+
const commitSourceEdit = (edit)=>{
|
|
51
|
+
onChange(edit.body);
|
|
52
|
+
requestAnimationFrame(()=>{
|
|
53
|
+
var _sourceRef_current, _sourceRef_current1;
|
|
54
|
+
(_sourceRef_current = sourceRef.current) == null ? void 0 : _sourceRef_current.focus();
|
|
55
|
+
(_sourceRef_current1 = sourceRef.current) == null ? void 0 : _sourceRef_current1.setSelectionRange(edit.start, edit.end);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
// One toolbar, two surfaces (AGL-985): in Visual a command mutates the
|
|
59
|
+
// editor's block model; in Markdown it wraps the textarea selection.
|
|
60
|
+
const handleCommand = (command)=>{
|
|
61
|
+
var _ref, _ref1;
|
|
62
|
+
if (mode === 'visual') {
|
|
63
|
+
var _visualRef_current;
|
|
64
|
+
(_visualRef_current = visualRef.current) == null ? void 0 : _visualRef_current.exec(command);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const input = sourceRef.current;
|
|
68
|
+
const start = (_ref = input == null ? void 0 : input.selectionStart) != null ? _ref : value.length;
|
|
69
|
+
const end = (_ref1 = input == null ? void 0 : input.selectionEnd) != null ? _ref1 : value.length;
|
|
70
|
+
// Both surfaces ask for a link the same way (AGL-3119), so a target
|
|
71
|
+
// picked here is the same reference the visual editor would insert.
|
|
72
|
+
if (command === 'link') {
|
|
73
|
+
setSourceLink({
|
|
74
|
+
start,
|
|
75
|
+
end,
|
|
76
|
+
text: value.slice(start, end)
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
commitSourceEdit(applyCommandToSource(value, start, end, command));
|
|
81
|
+
};
|
|
82
|
+
const handleSourceLink = (link)=>{
|
|
83
|
+
const target = sourceLink;
|
|
84
|
+
setSourceLink(null);
|
|
85
|
+
if (!target) return;
|
|
86
|
+
commitSourceEdit(applyLinkToSource(value, target.start, target.end, link));
|
|
87
|
+
};
|
|
88
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
89
|
+
spacing: 1,
|
|
90
|
+
children: [
|
|
91
|
+
/*#__PURE__*/ _jsx(Typography, {
|
|
92
|
+
variant: "subtitle2",
|
|
93
|
+
children: label
|
|
94
|
+
}),
|
|
95
|
+
/*#__PURE__*/ _jsx(MarkdownEditorToolbar, {
|
|
96
|
+
onCommand: handleCommand,
|
|
97
|
+
context: mode === 'visual' ? context : null,
|
|
98
|
+
mode: mode,
|
|
99
|
+
onModeChange: setMode
|
|
100
|
+
}),
|
|
101
|
+
mode === 'visual' ? // No wrapper box (AGL-982): the editor draws its own border and
|
|
102
|
+
// focus ring, so a second one read as a box inside a box.
|
|
103
|
+
/*#__PURE__*/ _jsx(MarkdownVisualEditor, {
|
|
104
|
+
ref: (handle)=>{
|
|
105
|
+
visualRef.current = handle;
|
|
106
|
+
editorRef == null ? void 0 : editorRef(handle ? {
|
|
107
|
+
insertImage: (alt, url)=>handle.insertImage(alt, url)
|
|
108
|
+
} : null);
|
|
109
|
+
},
|
|
110
|
+
value: value,
|
|
111
|
+
onChange: onChange,
|
|
112
|
+
onPickImageFromMedia: onPickImageFromMedia,
|
|
113
|
+
onContextChange: setContext,
|
|
114
|
+
minHeight: minHeight
|
|
115
|
+
}) : /*#__PURE__*/ _jsx(TextField, {
|
|
116
|
+
label: "Markdown source",
|
|
117
|
+
value: value,
|
|
118
|
+
onChange: (event)=>onChange(event.target.value),
|
|
119
|
+
size: "small",
|
|
120
|
+
multiline: true,
|
|
121
|
+
minRows: 12,
|
|
122
|
+
fullWidth: true,
|
|
123
|
+
inputRef: sourceRef,
|
|
124
|
+
helperText: MARKDOWN_SOURCE_HINT
|
|
125
|
+
}),
|
|
126
|
+
mode === 'visual' && helperText ? /*#__PURE__*/ _jsx(Typography, {
|
|
127
|
+
variant: "caption",
|
|
128
|
+
color: "text.secondary",
|
|
129
|
+
children: helperText
|
|
130
|
+
}) : null,
|
|
131
|
+
/*#__PURE__*/ _jsx(MarkdownLinkDialog, {
|
|
132
|
+
open: Boolean(sourceLink),
|
|
133
|
+
text: sourceLink == null ? void 0 : sourceLink.text,
|
|
134
|
+
withText: true,
|
|
135
|
+
onClose: ()=>setSourceLink(null),
|
|
136
|
+
onConfirm: handleSourceLink
|
|
137
|
+
})
|
|
138
|
+
]
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
MarkdownField.displayName = 'MarkdownField';
|
|
142
|
+
export default MarkdownField;
|
|
143
|
+
|
|
144
|
+
//# sourceMappingURL=markdown-field.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../libs/aglyn-markdown-editor/src/lib/markdown-field.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { Stack, TextField, Typography } from '@mui/material'\nimport { useRef, useState } from 'react'\nimport MarkdownEditorToolbar from './markdown-editor-toolbar.component'\nimport MarkdownLinkDialog, {\n type MarkdownLink,\n} from './markdown-link-dialog.component'\nimport {\n applyCommandToSource,\n applyLinkToSource,\n MARKDOWN_SOURCE_HINT,\n} from './markdown-source-command'\nimport MarkdownVisualEditor, {\n type MarkdownEditorCommand,\n type MarkdownEditorContext,\n type MarkdownVisualEditorHandle,\n} from './markdown-visual-editor.component'\n\nexport interface MarkdownFieldHandle {\n /** Insert an image at the cursor — used by a media picker upstream. */\n insertImage: (alt: string, url: string) => void\n}\n\nexport interface MarkdownFieldProps {\n label: string\n value: string\n onChange: (value: string) => void\n /**\n * Open the caller's media picker, with the alt text the author typed in the\n * image dialog (AGL-1645). Omitted when there is nowhere to pick from — the\n * toolbar's image action is then the editor's own prompt.\n */\n onPickImageFromMedia?: (alt: string) => void\n minHeight?: number\n /** Shown under the editor in visual mode. */\n helperText?: string\n}\n\n/**\n * A markdown field: toolbar, visual editor, and a raw-source escape hatch\n * (AGL-1080).\n *\n * This plumbing — one toolbar driving two surfaces, a mode switch, a\n * source-mode command path that wraps the textarea selection, and a handle\n * for inserting picked media — lived only inside the listing detail editor.\n * The publish form, which is where a README is written for the FIRST time,\n * had a four-row textarea instead: the worst tool on the first pass and the\n * good one on the second, for the same field.\n *\n * Copying sixty lines into the second caller is how two surfaces for one\n * piece of content drift apart, which is the complaint this closes. So it\n * is extracted once and used twice.\n *\n * The media picker itself stays with the caller: it needs a scope (an org\n * or a host) that this component has no business knowing.\n */\nexport function MarkdownField(\n props: MarkdownFieldProps & { editorRef?: (handle: MarkdownFieldHandle | null) => void },\n) {\n const {\n label,\n value,\n onChange,\n onPickImageFromMedia,\n minHeight = 200,\n helperText,\n editorRef,\n } = props\n const visualRef = useRef<MarkdownVisualEditorHandle | null>(null)\n const sourceRef = useRef<HTMLTextAreaElement | null>(null)\n // Visual by default, raw markdown one click away (AGL-985) — publishers\n // often arrive holding a README they wrote somewhere else.\n const [mode, setMode] = useState<'visual' | 'markdown'>('visual')\n const [context, setContext] = useState<MarkdownEditorContext | null>(null)\n /** The source-mode selection a link is being written over (AGL-3119). */\n const [sourceLink, setSourceLink] = useState<{\n start: number\n end: number\n text: string\n } | null>(null)\n\n const commitSourceEdit = (edit: { body: string; start: number; end: number }) => {\n onChange(edit.body)\n requestAnimationFrame(() => {\n sourceRef.current?.focus()\n sourceRef.current?.setSelectionRange(edit.start, edit.end)\n })\n }\n\n // One toolbar, two surfaces (AGL-985): in Visual a command mutates the\n // editor's block model; in Markdown it wraps the textarea selection.\n const handleCommand = (command: MarkdownEditorCommand) => {\n if (mode === 'visual') {\n visualRef.current?.exec(command)\n return\n }\n const input = sourceRef.current\n const start = input?.selectionStart ?? value.length\n const end = input?.selectionEnd ?? value.length\n // Both surfaces ask for a link the same way (AGL-3119), so a target\n // picked here is the same reference the visual editor would insert.\n if (command === 'link') {\n setSourceLink({ start, end, text: value.slice(start, end) })\n return\n }\n commitSourceEdit(applyCommandToSource(value, start, end, command))\n }\n\n const handleSourceLink = (link: MarkdownLink) => {\n const target = sourceLink\n setSourceLink(null)\n if (!target) return\n commitSourceEdit(applyLinkToSource(value, target.start, target.end, link))\n }\n\n return (\n <Stack spacing={1}>\n <Typography variant=\"subtitle2\">{label}</Typography>\n <MarkdownEditorToolbar\n onCommand={handleCommand}\n context={mode === 'visual' ? context : null}\n mode={mode}\n onModeChange={setMode}\n />\n {mode === 'visual' ? (\n // No wrapper box (AGL-982): the editor draws its own border and\n // focus ring, so a second one read as a box inside a box.\n <MarkdownVisualEditor\n ref={(handle) => {\n visualRef.current = handle\n editorRef?.(\n handle\n ? { insertImage: (alt, url) => handle.insertImage(alt, url) }\n : null,\n )\n }}\n value={value}\n onChange={onChange}\n onPickImageFromMedia={onPickImageFromMedia}\n onContextChange={setContext}\n minHeight={minHeight}\n />\n ) : (\n <TextField\n label=\"Markdown source\"\n value={value}\n onChange={(event) => onChange(event.target.value)}\n size=\"small\"\n multiline\n minRows={12}\n fullWidth\n inputRef={sourceRef}\n helperText={MARKDOWN_SOURCE_HINT}\n />\n )}\n {mode === 'visual' && helperText ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {helperText}\n </Typography>\n ) : null}\n <MarkdownLinkDialog\n open={Boolean(sourceLink)}\n text={sourceLink?.text}\n withText\n onClose={() => setSourceLink(null)}\n onConfirm={handleSourceLink}\n />\n </Stack>\n )\n}\n\nMarkdownField.displayName = 'MarkdownField'\n\nexport default MarkdownField\n"],"names":["Stack","TextField","Typography","useRef","useState","MarkdownEditorToolbar","MarkdownLinkDialog","applyCommandToSource","applyLinkToSource","MARKDOWN_SOURCE_HINT","MarkdownVisualEditor","MarkdownField","props","label","value","onChange","onPickImageFromMedia","minHeight","helperText","editorRef","visualRef","sourceRef","mode","setMode","context","setContext","sourceLink","setSourceLink","commitSourceEdit","edit","body","requestAnimationFrame","current","focus","setSelectionRange","start","end","handleCommand","command","exec","input","selectionStart","length","selectionEnd","text","slice","handleSourceLink","link","target","spacing","variant","onCommand","onModeChange","ref","handle","insertImage","alt","url","onContextChange","event","size","multiline","minRows","fullWidth","inputRef","color","open","Boolean","withText","onClose","onConfirm","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* HTML clipboard → markdown-lite conversion for the visual editor
|
|
19
|
+
* (AGL-596). Pure functions with no editor state: `htmlToRows` parses a
|
|
20
|
+
* `text/html` clipboard payload and maps it onto the editor's row model,
|
|
21
|
+
* keeping only what the markdown-lite dialect can express — bold, italic,
|
|
22
|
+
* links, h2/h3 headings, flat bullet and numbered lists (AGL-1320), images,
|
|
23
|
+
* blockquotes (AGL-1315), and (AGL-981) code blocks and tables, the two
|
|
24
|
+
* blocks a pasted README is made of. Everything else flattens
|
|
25
|
+
* to plain text; nested marks flatten to the OUTERMOST mark. Row keys are
|
|
26
|
+
* module-local placeholders — the editor re-keys converted rows with its
|
|
27
|
+
* own key sequence on ingest.
|
|
28
|
+
*/
|
|
29
|
+
import type { MarkdownInline } from '@aglyn/aglyn';
|
|
30
|
+
import type { EditorRow } from './markdown-visual-editor.component';
|
|
31
|
+
/** Converts an element's children to markdown-lite inlines. */
|
|
32
|
+
export declare function htmlToInlines(node: Node): MarkdownInline[];
|
|
33
|
+
/**
|
|
34
|
+
* Converts a `text/html` clipboard payload to editor rows. Unknown
|
|
35
|
+
* wrappers recurse so they never swallow content; rows with no inlines
|
|
36
|
+
* and no image are dropped.
|
|
37
|
+
*/
|
|
38
|
+
export declare function htmlToRows(html: string): EditorRow[];
|