diff --git a/.oxlintrc.json b/.oxlintrc.json index 1467ddeb906..a46722ec305 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -83,7 +83,7 @@ } ], "import/named": "error", - "import/sort-imports": "error", + "import/order": "error", "import/no-duplicates": "error", "import/no-import-assign": "error", "import/no-self-import": "error", diff --git a/packages/components/src/__internal__/icon.tsx b/packages/components/src/__internal__/components/icon.tsx similarity index 95% rename from packages/components/src/__internal__/icon.tsx rename to packages/components/src/__internal__/components/icon.tsx index 3c15cb11c89..58b5aa2082c 100644 --- a/packages/components/src/__internal__/icon.tsx +++ b/packages/components/src/__internal__/components/icon.tsx @@ -4,7 +4,7 @@ import { h } from 'vue' h type IconProps = { - icon: string | null + icon?: string | null class?: string } diff --git a/packages/components/src/__internal__/components/image-input.tsx b/packages/components/src/__internal__/components/image-input.tsx new file mode 100644 index 00000000000..eeb9aca3dcd --- /dev/null +++ b/packages/components/src/__internal__/components/image-input.tsx @@ -0,0 +1,163 @@ +import { defineComponent, ref, h, type Ref } from 'vue' +import clsx from 'clsx' +import { customAlphabet } from 'nanoid' +import { Icon } from './icon' + +h + +const nanoid = customAlphabet('abcdefg', 8) + +type ImageInputProps = { + src: Ref + selected: Ref + readonly: Ref + setLink: (link: string) => void + + imageIcon?: string + uploadButton?: string + confirmButton?: string + uploadPlaceholderText?: string + + className?: string + + onUpload: (file: File) => Promise +} + +export const ImageInput = defineComponent({ + props: { + src: { + type: Object, + required: true, + }, + selected: { + type: Object, + required: true, + }, + readonly: { + type: Object, + required: true, + }, + setLink: { + type: Function, + required: true, + }, + imageIcon: { + type: String, + required: false, + }, + uploadButton: { + type: String, + required: false, + }, + confirmButton: { + type: String, + required: false, + }, + uploadPlaceholderText: { + type: String, + required: false, + }, + onUpload: { + type: Function, + required: true, + }, + }, + setup({ + readonly, + src, + setLink, + onUpload, + imageIcon, + uploadButton, + confirmButton, + uploadPlaceholderText, + className, + }) { + const focusLinkInput = ref(false) + const linkInputRef = ref() + const currentLink = ref(src.value ?? '') + const uuid = ref(nanoid()) + const hidePlaceholder = ref(src.value?.length !== 0) + const onEditLink = (e: Event) => { + const target = e.target as HTMLInputElement + const value = target.value + hidePlaceholder.value = value.length !== 0 + currentLink.value = value + } + + const onKeydown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + setLink(linkInputRef.value?.value ?? '') + } + } + + const onConfirmLinkInput = () => { + setLink(linkInputRef.value?.value ?? '') + } + + const onUploadFile = (e: Event) => { + const file = (e.target as HTMLInputElement).files?.[0] + if (!file) return + + onUpload(file) + .then((url) => { + if (!url) return + + setLink(url) + hidePlaceholder.value = true + }) + .catch((err) => { + console.error('An error occurred while uploading image') + console.error(err) + }) + } + + return () => { + return ( +
+ + + {currentLink.value && ( +
onConfirmLinkInput()}> + +
+ )} +
+ ) + } + }, +}) diff --git a/packages/components/src/image-block/config.ts b/packages/components/src/image-block/config.ts index 089bcc88f96..80d7f23cb9f 100644 --- a/packages/components/src/image-block/config.ts +++ b/packages/components/src/image-block/config.ts @@ -2,10 +2,10 @@ import { $ctx } from '@milkdown/utils' import { withMeta } from '../__internal__/meta' export interface ImageBlockConfig { - imageIcon: () => string | null - captionIcon: () => string | null - uploadButton: () => string | null - confirmButton: () => string | null + imageIcon: () => string | undefined + captionIcon: () => string | undefined + uploadButton: () => string | undefined + confirmButton: () => string | undefined uploadPlaceholderText: string captionPlaceholderText: string onUpload: (file: File) => Promise diff --git a/packages/components/src/image-block/view/components/image-block.tsx b/packages/components/src/image-block/view/components/image-block.tsx index e4807681b2f..34e5e76f7f9 100644 --- a/packages/components/src/image-block/view/components/image-block.tsx +++ b/packages/components/src/image-block/view/components/image-block.tsx @@ -1,7 +1,7 @@ import { h, Fragment, type Ref, defineComponent } from 'vue' import type { ImageBlockConfig } from '../../config' import { ImageViewer } from './image-viewer' -import { ImageInput } from './image-input' +import { ImageInput } from '../../../__internal__/components/image-input' h Fragment @@ -13,8 +13,8 @@ type Attrs = { } export type MilkdownImageBlockProps = { - selected: Ref - readonly: Ref + selected: Ref + readonly: Ref setAttr: (attr: T, value: Attrs[T]) => void config: ImageBlockConfig } & { @@ -57,7 +57,19 @@ export const MilkdownImageBlock = defineComponent({ return () => { if (!src.value?.length) { - return + return ( + props.setAttr('src', link)} + imageIcon={props.config.imageIcon()} + uploadButton={props.config.uploadButton()} + confirmButton={props.config.confirmButton()} + uploadPlaceholderText={props.config.uploadPlaceholderText} + onUpload={props.config.onUpload} + /> + ) } return } diff --git a/packages/components/src/image-block/view/components/image-input.tsx b/packages/components/src/image-block/view/components/image-input.tsx index 54049c4df2a..9967ee8c130 100644 --- a/packages/components/src/image-block/view/components/image-input.tsx +++ b/packages/components/src/image-block/view/components/image-input.tsx @@ -2,7 +2,7 @@ import { defineComponent, ref, h } from 'vue' import type { MilkdownImageBlockProps } from './image-block' import clsx from 'clsx' import { customAlphabet } from 'nanoid' -import { Icon } from '../../../__internal__/icon' +import { Icon } from '../../../__internal__/components/icon' h diff --git a/packages/components/src/image-block/view/components/image-viewer.tsx b/packages/components/src/image-block/view/components/image-viewer.tsx index 2a90bde6e18..a0cc117bcab 100644 --- a/packages/components/src/image-block/view/components/image-viewer.tsx +++ b/packages/components/src/image-block/view/components/image-viewer.tsx @@ -1,7 +1,7 @@ import { defineComponent, ref, h, Fragment } from 'vue' import type { MilkdownImageBlockProps } from './image-block' import { IMAGE_DATA_TYPE } from '../../schema' -import { Icon } from '../../../__internal__/icon' +import { Icon } from '../../../__internal__/components/icon' h Fragment diff --git a/packages/components/src/image-block/view/index.ts b/packages/components/src/image-block/view/index.ts index a806c4549e1..da379576e44 100644 --- a/packages/components/src/image-block/view/index.ts +++ b/packages/components/src/image-block/view/index.ts @@ -16,7 +16,7 @@ export const imageBlockView = $view( const ratio = ref(initialNode.attrs.ratio) const selected = ref(false) const readonly = ref(!view.editable) - const setAttr = (attr: string, value: string) => { + const setAttr = (attr: string, value: unknown) => { const pos = getPos() if (pos == null) return view.dispatch(view.state.tr.setNodeAttribute(pos, attr, value)) @@ -34,7 +34,7 @@ export const imageBlockView = $view( const dom = document.createElement('div') dom.className = 'milkdown-image-block' app.mount(dom) - const selectedWatcher = watchEffect(() => { + const disposeSelectedWatcher = watchEffect(() => { const isSelected = selected.value if (isSelected) { dom.classList.add('selected') @@ -63,7 +63,6 @@ export const imageBlockView = $view( } bindAttrs(initialNode) - selected.value = false return { dom, update: (updatedNode) => { @@ -84,7 +83,7 @@ export const imageBlockView = $view( selected.value = false }, destroy: () => { - selectedWatcher() + disposeSelectedWatcher() app.unmount() dom.remove() }, diff --git a/packages/components/src/image-inline/component.ts b/packages/components/src/image-inline/component.ts deleted file mode 100644 index 05225143c64..00000000000 --- a/packages/components/src/image-inline/component.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { c, html, useRef, useState } from 'atomico' -import type { Component } from 'atomico' -import { customAlphabet } from 'nanoid' -import clsx from 'clsx' -import type { InlineImageConfig } from './config' - -export interface Attrs { - src: string - alt: string - title: string -} - -export type InlineImageComponentProps = Attrs & { - setAttr: (attr: T, value: Attrs[T]) => void - selected: boolean - config: InlineImageConfig -} - -const nanoid = customAlphabet('abcdefg', 8) - -export const inlineImageComponent: Component = ({ - src = '', - selected = false, - alt, - title, - setAttr, - config, -}) => { - const linkInput = useRef() - const [uuid] = useState(nanoid()) - const [focusLinkInput, setFocusLinkInput] = useState(false) - const [hidePlaceholder, setHidePlaceholder] = useState(src.length !== 0) - const [currentLink, setCurrentLink] = useState(src) - - const onEditLink = (e: InputEvent) => { - const target = e.target as HTMLInputElement - const value = target.value - setHidePlaceholder(value.length !== 0) - setCurrentLink(value) - } - - const onUpload = async (e: InputEvent) => { - const file = (e.target as HTMLInputElement).files?.[0] - if (!file) return - - const url = await config?.onUpload(file) - if (!url) return - - setAttr?.('src', url) - setHidePlaceholder(true) - } - - const onConfirmLinkInput = () => { - setAttr?.('src', linkInput.current?.value ?? '') - } - - const onKeydown = (e: KeyboardEvent) => { - if (e.key === 'Enter') onConfirmLinkInput() - } - - const preventDrag = (e: Event) => { - e.preventDefault() - e.stopPropagation() - } - - return html` - ${!src - ? html`
-
${config?.imageIcon()}
- -
onConfirmLinkInput()} - > - ${config?.confirmButton()} -
-
` - : html`${alt}`} -
` -} - -inlineImageComponent.props = { - src: String, - alt: String, - title: String, - selected: Boolean, - setAttr: Function, - config: Object, -} - -export const InlineImageElement = c(inlineImageComponent) diff --git a/packages/components/src/image-inline/components/image-inline.tsx b/packages/components/src/image-inline/components/image-inline.tsx new file mode 100644 index 00000000000..2c39d146ab8 --- /dev/null +++ b/packages/components/src/image-inline/components/image-inline.tsx @@ -0,0 +1,83 @@ +import { h, Fragment, type Ref, defineComponent } from 'vue' +import type { InlineImageConfig } from '../config' +import { ImageInput } from '../../__internal__/components/image-input' + +h +Fragment + +type Attrs = { + src: string + alt: string + title: string +} + +export type MilkdownImageInlineProps = { + selected: Ref + readonly: Ref + setAttr: (attr: T, value: Attrs[T]) => void + config: InlineImageConfig +} & { + [P in keyof Attrs]: Ref +} + +export const MilkdownImageInline = defineComponent({ + props: { + src: { + type: Object, + required: true, + }, + alt: { + type: Object, + required: true, + }, + title: { + type: Object, + required: true, + }, + selected: { + type: Object, + required: true, + }, + readonly: { + type: Object, + required: true, + }, + setAttr: { + type: Function, + required: true, + }, + config: { + type: Object, + required: true, + }, + }, + setup(props) { + const { src, alt, title } = props + return () => { + if (!src.value?.length) { + return ( + props.setAttr('src', link)} + imageIcon={props.config.imageIcon()} + uploadButton={props.config.uploadButton()} + confirmButton={props.config.confirmButton()} + uploadPlaceholderText={props.config.uploadPlaceholderText} + onUpload={props.config.onUpload} + className="empty-image-inline" + /> + ) + } + return ( + {alt.value} + ) + } + }, +}) diff --git a/packages/components/src/image-inline/config.ts b/packages/components/src/image-inline/config.ts index c9b0fe793e9..da1b4899cef 100644 --- a/packages/components/src/image-inline/config.ts +++ b/packages/components/src/image-inline/config.ts @@ -3,9 +3,9 @@ import { html } from 'atomico' import { withMeta } from '../__internal__/meta' export interface InlineImageConfig { - imageIcon: () => ReturnType - uploadButton: () => ReturnType - confirmButton: () => ReturnType + imageIcon: () => string | undefined + uploadButton: () => string | undefined + confirmButton: () => string | undefined uploadPlaceholderText: string onUpload: (file: File) => Promise proxyDomURL?: (url: string) => Promise | string diff --git a/packages/components/src/image-inline/view.ts b/packages/components/src/image-inline/view.ts index c67f2a6683a..dda97c95169 100644 --- a/packages/components/src/image-inline/view.ts +++ b/packages/components/src/image-inline/view.ts @@ -3,46 +3,63 @@ import type { NodeViewConstructor } from '@milkdown/prose/view' import { imageSchema } from '@milkdown/preset-commonmark' import type { Node } from '@milkdown/prose/model' import { withMeta } from '../__internal__/meta' -import { defIfNotExists } from '../__internal__/helper' -import type { InlineImageComponentProps } from './component' -import { InlineImageElement } from './component' import { inlineImageConfig } from './config' +import { createApp, ref, watchEffect } from 'vue' +import { MilkdownImageInline } from './components/image-inline' -defIfNotExists('milkdown-image-inline', InlineImageElement) export const inlineImageView = $view( imageSchema.node, (ctx): NodeViewConstructor => { return (initialNode, view, getPos) => { - const dom = document.createElement( - 'milkdown-image-inline' - ) as HTMLElement & InlineImageComponentProps + const src = ref(initialNode.attrs.src) + const alt = ref(initialNode.attrs.alt) + const title = ref(initialNode.attrs.title) + const selected = ref(false) + const readonly = ref(!view.editable) + const setAttr = (attr: string, value: unknown) => { + const pos = getPos() + if (pos == null) return + view.dispatch(view.state.tr.setNodeAttribute(pos, attr, value)) + } const config = ctx.get(inlineImageConfig.key) + const app = createApp(MilkdownImageInline, { + src, + alt, + title, + selected, + readonly, + setAttr, + config, + }) + const dom = document.createElement('span') + dom.className = 'milkdown-image-inline' + app.mount(dom) + const disposeSelectedWatcher = watchEffect(() => { + const isSelected = selected.value + if (isSelected) { + dom.classList.add('selected') + } else { + dom.classList.remove('selected') + } + }) const proxyDomURL = config.proxyDomURL const bindAttrs = (node: Node) => { if (!proxyDomURL) { - dom.src = node.attrs.src + src.value = node.attrs.src } else { const proxiedURL = proxyDomURL(node.attrs.src) if (typeof proxiedURL === 'string') { - dom.src = proxiedURL + src.value = proxiedURL } else { proxiedURL.then((url) => { - dom.src = url + src.value = url }) } } - dom.alt = node.attrs.alt - dom.title = node.attrs.title + alt.value = node.attrs.alt + title.value = node.attrs.title } bindAttrs(initialNode) - dom.selected = false - dom.setAttr = (attr, value) => { - const pos = getPos() - if (pos == null) return - - view.dispatch(view.state.tr.setNodeAttribute(pos, attr, value)) - } - dom.config = config return { dom, update: (updatedNode) => { @@ -52,17 +69,19 @@ export const inlineImageView = $view( return true }, stopEvent: (e) => { - if (dom.selected && e.target instanceof HTMLInputElement) return true + if (e.target instanceof HTMLInputElement) return true return false }, selectNode: () => { - dom.selected = true + selected.value = true }, deselectNode: () => { - dom.selected = false + selected.value = false }, destroy: () => { + disposeSelectedWatcher() + app.unmount() dom.remove() }, } diff --git a/packages/crepe/src/feature/image-block/index.ts b/packages/crepe/src/feature/image-block/index.ts index 1edf1452007..920ed3ade5d 100644 --- a/packages/crepe/src/feature/image-block/index.ts +++ b/packages/crepe/src/feature/image-block/index.ts @@ -7,12 +7,7 @@ import { inlineImageConfig, } from '@milkdown/kit/component/image-inline' import type { DefineFeature, Icon } from '../shared' -import { - captionIcon, - confirmIcon, - imageIcon, - legacyImageIcon, -} from '../../icons' +import { captionIcon, imageIcon, confirmIcon } from '../../icons' interface ImageBlockConfig { onUpload: (file: File) => Promise @@ -43,7 +38,7 @@ export const defineFeature: DefineFeature = ( .config((ctx) => { ctx.update(inlineImageConfig.key, (value) => ({ uploadButton: config?.inlineUploadButton ?? (() => 'Upload'), - imageIcon: config?.inlineImageIcon ?? (() => legacyImageIcon), + imageIcon: config?.inlineImageIcon ?? (() => imageIcon), confirmButton: config?.inlineConfirmButton ?? (() => confirmIcon), uploadPlaceholderText: config?.inlineUploadPlaceholderText ?? 'or paste link', diff --git a/packages/crepe/src/feature/latex/index.ts b/packages/crepe/src/feature/latex/index.ts index 99445145453..f9beb621d1e 100644 --- a/packages/crepe/src/feature/latex/index.ts +++ b/packages/crepe/src/feature/latex/index.ts @@ -10,7 +10,7 @@ import { defIfNotExists } from '../../utils' import { LatexInlineEditElement } from './inline-tooltip/component' import { inlineLatexTooltip } from './inline-tooltip/tooltip' import { LatexInlineTooltip } from './inline-tooltip/view' -import { confirmIcon } from '../../icons' +import { legacyConfirmIcon } from '../../icons' import { mathBlockInputRule, mathInlineInputRule } from './input-rule' import { blockLatexSchema } from './block-latex' @@ -48,7 +48,8 @@ export const defineFeature: DefineFeature = ( ctx.set(inlineLatexTooltip.key, { view: (view) => { return new LatexInlineTooltip(ctx, view, { - inlineEditConfirm: config?.inlineEditConfirm ?? (() => confirmIcon), + inlineEditConfirm: + config?.inlineEditConfirm ?? (() => legacyConfirmIcon), ...config, }) }, diff --git a/packages/crepe/src/feature/link-tooltip/index.ts b/packages/crepe/src/feature/link-tooltip/index.ts index c2ad0b6c5d6..9d1dc11789b 100644 --- a/packages/crepe/src/feature/link-tooltip/index.ts +++ b/packages/crepe/src/feature/link-tooltip/index.ts @@ -4,7 +4,7 @@ import { linkTooltipPlugin, } from '@milkdown/kit/component/link-tooltip' import type { DefineFeature, Icon } from '../shared' -import { confirmIcon, copyIcon, editIcon, removeIcon } from '../../icons' +import { legacyConfirmIcon, copyIcon, editIcon, removeIcon } from '../../icons' interface LinkTooltipConfig { linkIcon: Icon @@ -29,7 +29,7 @@ export const defineFeature: DefineFeature = ( linkIcon: config?.linkIcon ?? (() => copyIcon), editButton: config?.editButton ?? (() => editIcon), removeButton: config?.removeButton ?? (() => removeIcon), - confirmButton: config?.confirmButton ?? (() => confirmIcon), + confirmButton: config?.confirmButton ?? (() => legacyConfirmIcon), inputPlaceholder: config?.inputPlaceholder ?? 'Paste link...', onCopyLink: config?.onCopyLink ?? (() => {}), })) diff --git a/packages/crepe/src/feature/shared.ts b/packages/crepe/src/feature/shared.ts index 2cd5b24726f..9df7ef1364d 100644 --- a/packages/crepe/src/feature/shared.ts +++ b/packages/crepe/src/feature/shared.ts @@ -5,4 +5,4 @@ export type DefineFeature = ( config?: Config ) => void | Promise -export type Icon = () => string | null +export type Icon = () => string | undefined diff --git a/packages/crepe/src/icons/confirm.ts b/packages/crepe/src/icons/confirm.ts index a3bb4f6049b..cca6184fbf3 100644 --- a/packages/crepe/src/icons/confirm.ts +++ b/packages/crepe/src/icons/confirm.ts @@ -1,6 +1,28 @@ import { html } from 'atomico' -export const confirmIcon = html` +export const confirmIcon = ` + + + + + + + + + + +` + +export const legacyConfirmIcon = html` .empty-image-inline { display: inline-flex; } @@ -30,6 +27,7 @@ & > .empty-image-inline .link-importer > .link-input-area { width: 208px; color: var(--crepe-color-on-background); + display: flex; } & > .empty-image-inline .link-importer .placeholder { @@ -67,10 +65,6 @@ } } - img.image-inline { - vertical-align: text-bottom; - } - .empty-image-inline { align-items: center; padding: 4px 10px; @@ -78,6 +72,7 @@ background: var(--crepe-color-surface); font-family: var(--crepe-font-default); border-radius: 8px; + font-size: 16px; } .empty-image-inline .image-icon { @@ -135,6 +130,7 @@ width: 18px; height: 18px; } + display: flex; width: 24px; height: 24px; padding: 3px; diff --git a/storybook/stories/components/inline-image-block.css b/storybook/stories/components/inline-image-block.css index 3ebc12213c0..f8ee81506b6 100644 --- a/storybook/stories/components/inline-image-block.css +++ b/storybook/stories/components/inline-image-block.css @@ -1,14 +1,19 @@ .milkdown-storybook { - milkdown-image-inline.selected > .image-inline { + .milkdown-image-inline { + outline: none; + display: inline-flex; + vertical-align: text-bottom; + } + + .milkdown-image-inline.selected > .image-inline { outline: 2px solid fuchsia; - vertical-align: bottom; } - milkdown-image-inline.selected > .empty-image-inline { + .milkdown-image-inline.selected > .empty-image-inline { outline: 2px solid fuchsia; } - milkdown-image-inline > .empty-image-inline { + .milkdown-image-inline > .empty-image-inline { align-items: center; padding: 0 8px; gap: 8px; @@ -16,27 +21,27 @@ border-radius: 4px; } - milkdown-image-inline > .empty-image-inline .image-icon { + .milkdown-image-inline > .empty-image-inline .image-icon { color: darkgray; } - milkdown-image-inline > .empty-image-inline .image-icon svg { + .milkdown-image-inline > .empty-image-inline .image-icon svg { width: 24px; height: 24px; } - milkdown-image-inline > .empty-image-inline .link-importer .placeholder { + .milkdown-image-inline > .empty-image-inline .link-importer .placeholder { color: darkgray; font-size: 12px; } - milkdown-image-inline > .empty-image-inline .link-importer .link-input-area { + .milkdown-image-inline > .empty-image-inline .link-importer .link-input-area { line-height: 16px; font-size: 12px; padding: 8px 0; } - milkdown-image-inline + .milkdown-image-inline > .empty-image-inline .link-importer .placeholder @@ -47,7 +52,7 @@ transition: color 0.2s; } - milkdown-image-inline + .milkdown-image-inline > .empty-image-inline .link-importer.focus .placeholder @@ -55,7 +60,7 @@ color: unset; } - milkdown-image-inline + .milkdown-image-inline > .empty-image-inline .link-importer .placeholder @@ -63,7 +68,7 @@ color: fuchsia; } - milkdown-image-inline + .milkdown-image-inline > .empty-image-inline .link-importer .placeholder @@ -71,7 +76,7 @@ margin-left: 8px; } - milkdown-image-inline > .empty-image-inline .confirm { + .milkdown-image-inline > .empty-image-inline .confirm { background: darkgray; color: antiquewhite; line-height: 28px; @@ -80,42 +85,38 @@ font-size: 12px; } - milkdown-image-inline { + .milkdown-image-inline { outline: none; display: inline; } - milkdown-image-inline input { + .milkdown-image-inline input { background: transparent; outline: none; border: 0; } - milkdown-image-inline.empty { - vertical-align: bottom; - } - - milkdown-image-inline > .empty-image-inline { + .milkdown-image-inline > .empty-image-inline { display: inline-flex; } - milkdown-image-inline > .empty-image-inline .confirm { + .milkdown-image-inline > .empty-image-inline .confirm { cursor: pointer; } - milkdown-image-inline > .empty-image-inline .link-importer { + .milkdown-image-inline > .empty-image-inline .link-importer { position: relative; flex: 1; } - milkdown-image-inline + .milkdown-image-inline > .empty-image-inline .link-importer > .link-input-area { width: 208px; } - milkdown-image-inline > .empty-image-inline .link-importer .placeholder { + .milkdown-image-inline > .empty-image-inline .link-importer .placeholder { position: absolute; top: 0; left: 0; @@ -125,7 +126,7 @@ cursor: text; } - milkdown-image-inline + .milkdown-image-inline > .empty-image-inline .link-importer .placeholder @@ -134,7 +135,7 @@ display: flex; } - milkdown-image-inline .hidden { + .milkdown-image-inline .hidden { display: none !important; } }