@soroush.tech/markdown 0.1.0
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 +21 -0
- package/README.md +179 -0
- package/dist/CodeBlock-CVlrvVPP.mjs +159 -0
- package/dist/CodeBlock-Cymx1ZAY.cjs +176 -0
- package/dist/CodeBlock.cjs +5 -0
- package/dist/CodeBlock.d.cts +2 -0
- package/dist/CodeBlock.d.mts +2 -0
- package/dist/CodeBlock.mjs +2 -0
- package/dist/Control.cjs +61 -0
- package/dist/Control.d.cts +18 -0
- package/dist/Control.d.mts +18 -0
- package/dist/Control.mjs +60 -0
- package/dist/Editor.cjs +129 -0
- package/dist/Editor.d.cts +40 -0
- package/dist/Editor.d.mts +40 -0
- package/dist/Editor.mjs +128 -0
- package/dist/MarkdownContext.cjs +13 -0
- package/dist/MarkdownContext.d.cts +2 -0
- package/dist/MarkdownContext.d.mts +2 -0
- package/dist/MarkdownContext.mjs +11 -0
- package/dist/Mermaid-BclI9A7e.mjs +231 -0
- package/dist/Mermaid-DqP2Q0bt.cjs +236 -0
- package/dist/Mermaid.cjs +3 -0
- package/dist/Mermaid.d.cts +2 -0
- package/dist/Mermaid.d.mts +2 -0
- package/dist/Mermaid.mjs +2 -0
- package/dist/Preview-BVT70uXa.cjs +276 -0
- package/dist/Preview-C1CwRDj5.mjs +246 -0
- package/dist/Preview.cjs +3 -0
- package/dist/Preview.d.cts +53 -0
- package/dist/Preview.d.mts +53 -0
- package/dist/Preview.mjs +2 -0
- package/dist/Toolbar-Ba3l-oBU.cjs +600 -0
- package/dist/Toolbar-C0SNI-JK.mjs +589 -0
- package/dist/Toolbar.cjs +4 -0
- package/dist/Toolbar.d.cts +13 -0
- package/dist/Toolbar.d.mts +13 -0
- package/dist/Toolbar.mjs +2 -0
- package/dist/applyAction-BDkgYhOd.cjs +120 -0
- package/dist/applyAction-BGWMbUYT.mjs +115 -0
- package/dist/index-BO0LyUUG.d.cts +27 -0
- package/dist/index-BO0LyUUG.d.mts +27 -0
- package/dist/index-BtMTBOcM.d.cts +48 -0
- package/dist/index-BtMTBOcM.d.mts +48 -0
- package/dist/index-rGZIhUgj.d.cts +76 -0
- package/dist/index-rGZIhUgj.d.mts +76 -0
- package/dist/index.cjs +19 -0
- package/dist/index.d.cts +50 -0
- package/dist/index.d.mts +50 -0
- package/dist/index.mjs +8 -0
- package/package.json +110 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Masoud Soroush <https://soroush.tech>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/@soroush.tech/markdown)
|
|
2
|
+
[](https://www.npmjs.com/package/@soroush.tech/markdown)
|
|
3
|
+
[](https://app.codecov.io/gh/soroush-tech/core?flags%5B0%5D=markdown)
|
|
4
|
+
[](https://www.npmjs.com/package/@soroush.tech/markdown)
|
|
5
|
+
[](https://www.npmjs.com/package/@soroush.tech/markdown)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
## A **headless markdown editor and preview**.
|
|
9
|
+
|
|
10
|
+
Compose a toolbar, a source editor, and a live preview however your UI needs — and every element is styled by your theme. The renderer works standalone too, turning any markdown string into themed components, with support for tables, task lists, strikethrough, autolinks, syntax-highlighted code, and mermaid diagrams.
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
## Why headless?
|
|
15
|
+
|
|
16
|
+
Most markdown editors hand you a fixed widget — a bordered box with a toolbar bolted on top and an Edit/Preview toggle you can't move. This one gives you the **pieces** instead:
|
|
17
|
+
|
|
18
|
+
- **You own the layout.** Put the preview beside the editor, below it, in a modal, or drop it entirely — the parts don't care.
|
|
19
|
+
- **You own the state.** The value is yours (`useState`, a form, a store); the editor just reads and writes it.
|
|
20
|
+
- **It looks like your app.** Output is themed components — `Typography`, `Link`, `Table`, and its own `CodeBlock` — not unstyled HTML, so it inherits your theme with zero extra CSS.
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **GitHub-Flavored Markdown** — tables, task lists, strikethrough, and autolinks.
|
|
25
|
+
- **Syntax-highlighted code** — fenced blocks are highlighted automatically; unknown languages degrade gracefully.
|
|
26
|
+
- **Mermaid diagrams** — a ` ```mermaid ` fence renders as a themed diagram with built-in zoom and pan controls, loaded lazily on the client and falling back to the source until it does.
|
|
27
|
+
- **A real editing surface** — Tab-to-indent, selection tracking, caret restoration, formatting buttons, heading and code-language selects, a hover-grid table picker, and a diagram picker that inserts starter templates for 23 mermaid diagram types.
|
|
28
|
+
- **Composable parts** — `Control`, `Toolbar`, `Editor`, and `Preview`, coordinated through React context.
|
|
29
|
+
- **Themeable to the slot** — restyle the editor, toolbar, and preview through your theme, no forking required.
|
|
30
|
+
- **Typed and SSR-safe** — first-class TypeScript, 100% test coverage, no browser globals at module scope.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
# npm
|
|
36
|
+
npm install @soroush.tech/markdown @soroush.tech/design-system
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
# pnpm
|
|
41
|
+
pnpm add @soroush.tech/markdown @soroush.tech/design-system
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
# yarn
|
|
46
|
+
yarn add @soroush.tech/markdown @soroush.tech/design-system
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> **Requires the design system.** This package renders through `@soroush.tech/design-system`, so its components must live inside that library's `ThemeProvider`. If you aren't already using the design system, [start there first](https://www.npmjs.com/package/@soroush.tech/design-system).
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
Wrap your app in the design system's `ThemeProvider` once, then render markdown anywhere inside it:
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { ThemeProvider } from '@soroush.tech/design-system/theme'
|
|
57
|
+
import { Preview } from '@soroush.tech/markdown'
|
|
58
|
+
|
|
59
|
+
export function App() {
|
|
60
|
+
return (
|
|
61
|
+
<ThemeProvider>
|
|
62
|
+
<Preview>
|
|
63
|
+
{
|
|
64
|
+
'# Hello\n\nRendered through your **theme** — tables, `code`, and [links](https://soroush.tech) included.'
|
|
65
|
+
}
|
|
66
|
+
</Preview>
|
|
67
|
+
</ThemeProvider>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
That's the whole renderer. To let people _write_ markdown, add the editing parts.
|
|
73
|
+
|
|
74
|
+
## Editor
|
|
75
|
+
|
|
76
|
+
The `Editor` works standalone via `value`/`onChange` — no `Control` needed:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { useState } from 'react'
|
|
80
|
+
import { Editor } from '@soroush.tech/markdown'
|
|
81
|
+
import { ThemeProvider } from '@soroush.tech/design-system/theme'
|
|
82
|
+
|
|
83
|
+
function MarkdownField() {
|
|
84
|
+
const [value, setValue] = useState('# Start writing\n\nJust the source editor — no preview.')
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<ThemeProvider>
|
|
88
|
+
<Editor value={value} onChange={setValue} />
|
|
89
|
+
</ThemeProvider>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## A full editor and preview in a split screen
|
|
95
|
+
|
|
96
|
+
Everything inside `Control` shares one value and selection, so you arrange the panes freely. Here's a toolbar above a side-by-side source and live preview:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { useState } from 'react'
|
|
100
|
+
import { Control, Toolbar, Editor, Preview } from '@soroush.tech/markdown'
|
|
101
|
+
import { Flex } from '@soroush.tech/design-system/Flex'
|
|
102
|
+
|
|
103
|
+
function MarkdownField() {
|
|
104
|
+
const [value, setValue] = useState(
|
|
105
|
+
'# Start writing\n\nType on the left, watch it render on the right. →'
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<Control value={value} onChange={setValue}>
|
|
110
|
+
<Toolbar />
|
|
111
|
+
<Flex gap={4}>
|
|
112
|
+
<Editor />
|
|
113
|
+
<Preview>{value}</Preview>
|
|
114
|
+
</Flex>
|
|
115
|
+
</Control>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
There is intentionally **no Edit·Split·Preview switch** — which panes to show, and when, is your call.
|
|
121
|
+
|
|
122
|
+
## The parts
|
|
123
|
+
|
|
124
|
+
| Part | Role |
|
|
125
|
+
| --------- | --------------------------------------------------------------------------------- |
|
|
126
|
+
| `Control` | Headless provider — owns the `value`/selection state. Takes `value` + `onChange`. |
|
|
127
|
+
| `Toolbar` | Formatting buttons, heading/code-language selects, and the table picker. |
|
|
128
|
+
| `Editor` | The source `<textarea>` — Tab-to-indent, selection, caret restore. |
|
|
129
|
+
| `Preview` | Renders a markdown string (its child) to themed components. |
|
|
130
|
+
|
|
131
|
+
`Toolbar` and `Editor` must be rendered inside `Control`; `Preview` works anywhere. Import each directly from the package — there is no `Markdown.*` namespace. Full prop references live in the [source on GitHub](https://github.com/soroush-tech/core/tree/main/packages/markdown/src).
|
|
132
|
+
|
|
133
|
+
## Customizing the look
|
|
134
|
+
|
|
135
|
+
Everything is styled through the design system's theme, and there are two levels to reach for — a shared theme for the whole look, and per-component slots for the markdown parts specifically.
|
|
136
|
+
|
|
137
|
+
### Customize the theme — restyle everything
|
|
138
|
+
|
|
139
|
+
`Preview` renders design-system components that read theme tokens, so your theme already governs how markdown looks. Build one with `createTheme` — palette, spacing, type scale, radii — and pass it to `ThemeProvider`; the markdown output changes along with the rest of your UI, no markdown-specific setup. See the [design system's theming guide](https://www.npmjs.com/package/@soroush.tech/design-system) for the full token set.
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
import { ThemeProvider, createTheme, baseTheme } from '@soroush.tech/design-system/theme'
|
|
143
|
+
|
|
144
|
+
const theme = createTheme(baseTheme, {
|
|
145
|
+
palette: {
|
|
146
|
+
primary: {
|
|
147
|
+
main: '#7C3AED',
|
|
148
|
+
light: '#A78BFA',
|
|
149
|
+
dark: '#5B21B6',
|
|
150
|
+
contrastText: '#FFFFFF',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// <ThemeProvider theme={theme}> … </ThemeProvider>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Customize the components — target the markdown parts
|
|
159
|
+
|
|
160
|
+
For tweaks specific to the editor, toolbar, or preview, this package registers three themeable slots under `theme.components` — `MarkdownEditor`, `MarkdownPreview`, and `MarkdownToolbar`:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
const theme = createTheme(baseTheme, {
|
|
164
|
+
components: {
|
|
165
|
+
MarkdownPreview: { styleOverrides: { root: { maxWidth: '72ch' } } },
|
|
166
|
+
MarkdownToolbar: { styleOverrides: { root: { gap: 4 } } },
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The overrides are fully typed — importing the package registers the slots on the theme automatically.
|
|
172
|
+
|
|
173
|
+
## Building your own parts
|
|
174
|
+
|
|
175
|
+
Need a custom control or a word count? `useMarkdownContext()` exposes the same value, selection, and `dispatch` the built-in parts use — call it from any component inside `Control`.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
[MIT](./LICENSE) © [Soroush Tech](https://soroush.tech)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Button } from "@soroush.tech/design-system/Button";
|
|
4
|
+
import { Icon } from "@soroush.tech/design-system/Icon";
|
|
5
|
+
import { styled } from "@soroush.tech/design-system";
|
|
6
|
+
import { View } from "@soroush.tech/design-system/View";
|
|
7
|
+
//#region ../hooks/src/useCopyToClipboard.ts
|
|
8
|
+
/** How long the "copied" confirmation stays true before reverting, in ms. */
|
|
9
|
+
const COPIED_RESET_MS = 2e3;
|
|
10
|
+
/**
|
|
11
|
+
* Copy-to-clipboard with a transient `copied` flag. Guards the unavailable
|
|
12
|
+
* clipboard API and rejected writes so the caller never throws or gets stuck.
|
|
13
|
+
*/
|
|
14
|
+
function useCopyToClipboard(resetMs = COPIED_RESET_MS) {
|
|
15
|
+
const [copied, setCopied] = useState(false);
|
|
16
|
+
const timerRef = useRef(null);
|
|
17
|
+
useEffect(() => () => {
|
|
18
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
19
|
+
}, []);
|
|
20
|
+
return {
|
|
21
|
+
copied,
|
|
22
|
+
copy: useCallback((text) => {
|
|
23
|
+
if (!navigator.clipboard?.writeText) return;
|
|
24
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
25
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
26
|
+
setCopied(true);
|
|
27
|
+
timerRef.current = setTimeout(() => {
|
|
28
|
+
setCopied(false);
|
|
29
|
+
timerRef.current = null;
|
|
30
|
+
}, resetMs);
|
|
31
|
+
}).catch(() => {
|
|
32
|
+
console.error("Clipboard write failed; leave `copied` in its idle state.");
|
|
33
|
+
});
|
|
34
|
+
}, [resetMs])
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/CodeBlock/CodeBlock.tsx
|
|
39
|
+
const syntaxStyles = ({ theme }) => ({
|
|
40
|
+
color: theme.syntax.base,
|
|
41
|
+
fontFamily: theme.syntax.font,
|
|
42
|
+
"& .hljs-comment, & .hljs-quote, & .hljs-doctag, & .hljs-meta.prompt_": {
|
|
43
|
+
color: theme.syntax.comment,
|
|
44
|
+
fontStyle: "italic"
|
|
45
|
+
},
|
|
46
|
+
"& .hljs-keyword, & .hljs-built_in, & .hljs-literal, & .hljs-selector-tag, & .hljs-selector-pseudo, & .hljs-selector-attr": { color: theme.syntax.keyword },
|
|
47
|
+
"& .hljs-type, & .hljs-title.class_": { color: theme.syntax.type },
|
|
48
|
+
"& .hljs-string, & .hljs-regexp, & .hljs-char, & .hljs-meta .hljs-string, & .hljs-addition": { color: theme.syntax.string },
|
|
49
|
+
"& .hljs-number, & .hljs-symbol, & .hljs-bullet, & .hljs-link": { color: theme.syntax.number },
|
|
50
|
+
"& .hljs-variable.constant_, & .hljs-property": { color: theme.syntax.constant },
|
|
51
|
+
"& .hljs-title, & .hljs-section, & .hljs-attr, & .hljs-attribute, & .hljs-selector-id": { color: theme.syntax.title },
|
|
52
|
+
"& .hljs-tag, & .hljs-name, & .hljs-meta, & .hljs-variable, & .hljs-selector-class, & .hljs-deletion": { color: theme.syntax.tag },
|
|
53
|
+
"& .hljs-emphasis": { fontStyle: "italic" },
|
|
54
|
+
"& .hljs-strong": { fontWeight: "bold" }
|
|
55
|
+
});
|
|
56
|
+
const CodeSurface = styled(View, {
|
|
57
|
+
name: "CodeBlock",
|
|
58
|
+
slot: "surface",
|
|
59
|
+
label: "CodeSurface"
|
|
60
|
+
})(syntaxStyles);
|
|
61
|
+
const Wrapper = styled(View, {
|
|
62
|
+
name: "CodeBlock",
|
|
63
|
+
label: "CodeBlock"
|
|
64
|
+
})({
|
|
65
|
+
display: "grid",
|
|
66
|
+
gridTemplateColumns: "minmax(0, 1fr)",
|
|
67
|
+
"& .code-copy": {
|
|
68
|
+
opacity: 0,
|
|
69
|
+
transition: "opacity 120ms ease"
|
|
70
|
+
},
|
|
71
|
+
"&:hover .code-copy, &:focus-within .code-copy": { opacity: 1 },
|
|
72
|
+
"@media (hover: none)": { "& .code-copy": { opacity: 1 } }
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Fenced-code block: a horizontally scrollable, syntax-highlighted surface with a
|
|
76
|
+
* copy-to-clipboard button that sticks to the top-right while the block is in view.
|
|
77
|
+
*/
|
|
78
|
+
function CodeBlock({ children, ...rest }) {
|
|
79
|
+
const surfaceRef = useRef(null);
|
|
80
|
+
const { copied, copy } = useCopyToClipboard();
|
|
81
|
+
const handleCopy = () => {
|
|
82
|
+
const raw = surfaceRef.current.textContent;
|
|
83
|
+
let end = raw.length;
|
|
84
|
+
while (end > 0 && raw[end - 1] === "\n") end--;
|
|
85
|
+
const text = raw.slice(0, end);
|
|
86
|
+
if (text) copy(text);
|
|
87
|
+
};
|
|
88
|
+
return /* @__PURE__ */ jsxs(Wrapper, {
|
|
89
|
+
my: 2,
|
|
90
|
+
position: "relative",
|
|
91
|
+
...rest,
|
|
92
|
+
children: [/* @__PURE__ */ jsx(View, {
|
|
93
|
+
position: "absolute",
|
|
94
|
+
top: "8px",
|
|
95
|
+
right: "8px",
|
|
96
|
+
zIndex: 1,
|
|
97
|
+
bg: "paper",
|
|
98
|
+
borderRadius: "sm",
|
|
99
|
+
className: "code-copy",
|
|
100
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
101
|
+
type: "button",
|
|
102
|
+
variant: "outlined",
|
|
103
|
+
color: "default",
|
|
104
|
+
size: "sm",
|
|
105
|
+
borderRadius: "sm",
|
|
106
|
+
onClick: handleCopy,
|
|
107
|
+
"aria-label": copied ? "Copied" : "Copy code",
|
|
108
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
109
|
+
name: copied ? "check" : "content_copy",
|
|
110
|
+
color: copied ? "success" : "primary",
|
|
111
|
+
size: "1.1rem"
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
}), /* @__PURE__ */ jsx(CodeSurface, {
|
|
115
|
+
as: "pre",
|
|
116
|
+
tabIndex: 0,
|
|
117
|
+
ref: surfaceRef,
|
|
118
|
+
bg: "terminal",
|
|
119
|
+
p: 3,
|
|
120
|
+
borderRadius: "md",
|
|
121
|
+
borderColor: "light",
|
|
122
|
+
borderWidth: "thin",
|
|
123
|
+
maxWidth: "100%",
|
|
124
|
+
overflow: "auto",
|
|
125
|
+
children
|
|
126
|
+
})]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/CodeBlock/CodeBlock.data.ts
|
|
131
|
+
const font = "'JetBrains Mono Variable', 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace";
|
|
132
|
+
/** Matched to the JetBrains "Islands Dark" editor scheme. */
|
|
133
|
+
const syntaxDark = {
|
|
134
|
+
base: "#BBBBBB",
|
|
135
|
+
keyword: "#CF8E6D",
|
|
136
|
+
string: "#6AAB73",
|
|
137
|
+
number: "#2AACB8",
|
|
138
|
+
title: "#6BA9FA",
|
|
139
|
+
constant: "#C77DBB",
|
|
140
|
+
type: "#2AACB8",
|
|
141
|
+
comment: "#919191",
|
|
142
|
+
tag: "#D5B778",
|
|
143
|
+
font
|
|
144
|
+
};
|
|
145
|
+
/** Matched to the JetBrains "Islands Light" editor scheme. */
|
|
146
|
+
const syntaxLight = {
|
|
147
|
+
base: "#080808",
|
|
148
|
+
keyword: "#0033B3",
|
|
149
|
+
string: "#067D17",
|
|
150
|
+
number: "#1750EB",
|
|
151
|
+
title: "#00627A",
|
|
152
|
+
constant: "#871094",
|
|
153
|
+
type: "#007E8A",
|
|
154
|
+
comment: "#8C8C8C",
|
|
155
|
+
tag: "#008077",
|
|
156
|
+
font
|
|
157
|
+
};
|
|
158
|
+
//#endregion
|
|
159
|
+
export { syntaxLight as n, CodeBlock as r, syntaxDark as t };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
let react = require("react");
|
|
2
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
3
|
+
let _soroush_tech_design_system_Button = require("@soroush.tech/design-system/Button");
|
|
4
|
+
let _soroush_tech_design_system_Icon = require("@soroush.tech/design-system/Icon");
|
|
5
|
+
let _soroush_tech_design_system = require("@soroush.tech/design-system");
|
|
6
|
+
let _soroush_tech_design_system_View = require("@soroush.tech/design-system/View");
|
|
7
|
+
//#region ../hooks/src/useCopyToClipboard.ts
|
|
8
|
+
/** How long the "copied" confirmation stays true before reverting, in ms. */
|
|
9
|
+
const COPIED_RESET_MS = 2e3;
|
|
10
|
+
/**
|
|
11
|
+
* Copy-to-clipboard with a transient `copied` flag. Guards the unavailable
|
|
12
|
+
* clipboard API and rejected writes so the caller never throws or gets stuck.
|
|
13
|
+
*/
|
|
14
|
+
function useCopyToClipboard(resetMs = COPIED_RESET_MS) {
|
|
15
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
16
|
+
const timerRef = (0, react.useRef)(null);
|
|
17
|
+
(0, react.useEffect)(() => () => {
|
|
18
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
19
|
+
}, []);
|
|
20
|
+
return {
|
|
21
|
+
copied,
|
|
22
|
+
copy: (0, react.useCallback)((text) => {
|
|
23
|
+
if (!navigator.clipboard?.writeText) return;
|
|
24
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
25
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
26
|
+
setCopied(true);
|
|
27
|
+
timerRef.current = setTimeout(() => {
|
|
28
|
+
setCopied(false);
|
|
29
|
+
timerRef.current = null;
|
|
30
|
+
}, resetMs);
|
|
31
|
+
}).catch(() => {
|
|
32
|
+
console.error("Clipboard write failed; leave `copied` in its idle state.");
|
|
33
|
+
});
|
|
34
|
+
}, [resetMs])
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/CodeBlock/CodeBlock.tsx
|
|
39
|
+
const syntaxStyles = ({ theme }) => ({
|
|
40
|
+
color: theme.syntax.base,
|
|
41
|
+
fontFamily: theme.syntax.font,
|
|
42
|
+
"& .hljs-comment, & .hljs-quote, & .hljs-doctag, & .hljs-meta.prompt_": {
|
|
43
|
+
color: theme.syntax.comment,
|
|
44
|
+
fontStyle: "italic"
|
|
45
|
+
},
|
|
46
|
+
"& .hljs-keyword, & .hljs-built_in, & .hljs-literal, & .hljs-selector-tag, & .hljs-selector-pseudo, & .hljs-selector-attr": { color: theme.syntax.keyword },
|
|
47
|
+
"& .hljs-type, & .hljs-title.class_": { color: theme.syntax.type },
|
|
48
|
+
"& .hljs-string, & .hljs-regexp, & .hljs-char, & .hljs-meta .hljs-string, & .hljs-addition": { color: theme.syntax.string },
|
|
49
|
+
"& .hljs-number, & .hljs-symbol, & .hljs-bullet, & .hljs-link": { color: theme.syntax.number },
|
|
50
|
+
"& .hljs-variable.constant_, & .hljs-property": { color: theme.syntax.constant },
|
|
51
|
+
"& .hljs-title, & .hljs-section, & .hljs-attr, & .hljs-attribute, & .hljs-selector-id": { color: theme.syntax.title },
|
|
52
|
+
"& .hljs-tag, & .hljs-name, & .hljs-meta, & .hljs-variable, & .hljs-selector-class, & .hljs-deletion": { color: theme.syntax.tag },
|
|
53
|
+
"& .hljs-emphasis": { fontStyle: "italic" },
|
|
54
|
+
"& .hljs-strong": { fontWeight: "bold" }
|
|
55
|
+
});
|
|
56
|
+
const CodeSurface = (0, _soroush_tech_design_system.styled)(_soroush_tech_design_system_View.View, {
|
|
57
|
+
name: "CodeBlock",
|
|
58
|
+
slot: "surface",
|
|
59
|
+
label: "CodeSurface"
|
|
60
|
+
})(syntaxStyles);
|
|
61
|
+
const Wrapper = (0, _soroush_tech_design_system.styled)(_soroush_tech_design_system_View.View, {
|
|
62
|
+
name: "CodeBlock",
|
|
63
|
+
label: "CodeBlock"
|
|
64
|
+
})({
|
|
65
|
+
display: "grid",
|
|
66
|
+
gridTemplateColumns: "minmax(0, 1fr)",
|
|
67
|
+
"& .code-copy": {
|
|
68
|
+
opacity: 0,
|
|
69
|
+
transition: "opacity 120ms ease"
|
|
70
|
+
},
|
|
71
|
+
"&:hover .code-copy, &:focus-within .code-copy": { opacity: 1 },
|
|
72
|
+
"@media (hover: none)": { "& .code-copy": { opacity: 1 } }
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Fenced-code block: a horizontally scrollable, syntax-highlighted surface with a
|
|
76
|
+
* copy-to-clipboard button that sticks to the top-right while the block is in view.
|
|
77
|
+
*/
|
|
78
|
+
function CodeBlock({ children, ...rest }) {
|
|
79
|
+
const surfaceRef = (0, react.useRef)(null);
|
|
80
|
+
const { copied, copy } = useCopyToClipboard();
|
|
81
|
+
const handleCopy = () => {
|
|
82
|
+
const raw = surfaceRef.current.textContent;
|
|
83
|
+
let end = raw.length;
|
|
84
|
+
while (end > 0 && raw[end - 1] === "\n") end--;
|
|
85
|
+
const text = raw.slice(0, end);
|
|
86
|
+
if (text) copy(text);
|
|
87
|
+
};
|
|
88
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Wrapper, {
|
|
89
|
+
my: 2,
|
|
90
|
+
position: "relative",
|
|
91
|
+
...rest,
|
|
92
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_soroush_tech_design_system_View.View, {
|
|
93
|
+
position: "absolute",
|
|
94
|
+
top: "8px",
|
|
95
|
+
right: "8px",
|
|
96
|
+
zIndex: 1,
|
|
97
|
+
bg: "paper",
|
|
98
|
+
borderRadius: "sm",
|
|
99
|
+
className: "code-copy",
|
|
100
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_soroush_tech_design_system_Button.Button, {
|
|
101
|
+
type: "button",
|
|
102
|
+
variant: "outlined",
|
|
103
|
+
color: "default",
|
|
104
|
+
size: "sm",
|
|
105
|
+
borderRadius: "sm",
|
|
106
|
+
onClick: handleCopy,
|
|
107
|
+
"aria-label": copied ? "Copied" : "Copy code",
|
|
108
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_soroush_tech_design_system_Icon.Icon, {
|
|
109
|
+
name: copied ? "check" : "content_copy",
|
|
110
|
+
color: copied ? "success" : "primary",
|
|
111
|
+
size: "1.1rem"
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodeSurface, {
|
|
115
|
+
as: "pre",
|
|
116
|
+
tabIndex: 0,
|
|
117
|
+
ref: surfaceRef,
|
|
118
|
+
bg: "terminal",
|
|
119
|
+
p: 3,
|
|
120
|
+
borderRadius: "md",
|
|
121
|
+
borderColor: "light",
|
|
122
|
+
borderWidth: "thin",
|
|
123
|
+
maxWidth: "100%",
|
|
124
|
+
overflow: "auto",
|
|
125
|
+
children
|
|
126
|
+
})]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/CodeBlock/CodeBlock.data.ts
|
|
131
|
+
const font = "'JetBrains Mono Variable', 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace";
|
|
132
|
+
/** Matched to the JetBrains "Islands Dark" editor scheme. */
|
|
133
|
+
const syntaxDark = {
|
|
134
|
+
base: "#BBBBBB",
|
|
135
|
+
keyword: "#CF8E6D",
|
|
136
|
+
string: "#6AAB73",
|
|
137
|
+
number: "#2AACB8",
|
|
138
|
+
title: "#6BA9FA",
|
|
139
|
+
constant: "#C77DBB",
|
|
140
|
+
type: "#2AACB8",
|
|
141
|
+
comment: "#919191",
|
|
142
|
+
tag: "#D5B778",
|
|
143
|
+
font
|
|
144
|
+
};
|
|
145
|
+
/** Matched to the JetBrains "Islands Light" editor scheme. */
|
|
146
|
+
const syntaxLight = {
|
|
147
|
+
base: "#080808",
|
|
148
|
+
keyword: "#0033B3",
|
|
149
|
+
string: "#067D17",
|
|
150
|
+
number: "#1750EB",
|
|
151
|
+
title: "#00627A",
|
|
152
|
+
constant: "#871094",
|
|
153
|
+
type: "#007E8A",
|
|
154
|
+
comment: "#8C8C8C",
|
|
155
|
+
tag: "#008077",
|
|
156
|
+
font
|
|
157
|
+
};
|
|
158
|
+
//#endregion
|
|
159
|
+
Object.defineProperty(exports, "CodeBlock", {
|
|
160
|
+
enumerable: true,
|
|
161
|
+
get: function() {
|
|
162
|
+
return CodeBlock;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
Object.defineProperty(exports, "syntaxDark", {
|
|
166
|
+
enumerable: true,
|
|
167
|
+
get: function() {
|
|
168
|
+
return syntaxDark;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
Object.defineProperty(exports, "syntaxLight", {
|
|
172
|
+
enumerable: true,
|
|
173
|
+
get: function() {
|
|
174
|
+
return syntaxLight;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_CodeBlock = require("./CodeBlock-Cymx1ZAY.cjs");
|
|
3
|
+
exports.CodeBlock = require_CodeBlock.CodeBlock;
|
|
4
|
+
exports.syntaxDark = require_CodeBlock.syntaxDark;
|
|
5
|
+
exports.syntaxLight = require_CodeBlock.syntaxLight;
|
package/dist/Control.cjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_applyAction = require("./applyAction-BDkgYhOd.cjs");
|
|
3
|
+
const require_MarkdownContext = require("./MarkdownContext.cjs");
|
|
4
|
+
let react = require("react");
|
|
5
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
//#region src/Control/Control.tsx
|
|
7
|
+
/**
|
|
8
|
+
* The headless root of the Markdown compound: owns the shared value/selection state and exposes
|
|
9
|
+
* it (plus a `dispatch` for toolbar actions) via context. It renders no UI of its own — the
|
|
10
|
+
* consumer arranges the parts and decides which panes to show.
|
|
11
|
+
*/
|
|
12
|
+
function Control({ value, onChange, children }) {
|
|
13
|
+
const lastSelectionRef = (0, react.useRef)(null);
|
|
14
|
+
const pendingSelectionRef = (0, react.useRef)(null);
|
|
15
|
+
const context = (0, react.useMemo)(() => {
|
|
16
|
+
const dispatch = (action) => {
|
|
17
|
+
const { start, end } = lastSelectionRef.current ?? {
|
|
18
|
+
start: value.length,
|
|
19
|
+
end: value.length
|
|
20
|
+
};
|
|
21
|
+
const next = require_applyAction.applyAction(action, {
|
|
22
|
+
value,
|
|
23
|
+
selectionStart: start,
|
|
24
|
+
selectionEnd: end
|
|
25
|
+
});
|
|
26
|
+
const selection = {
|
|
27
|
+
start: next.selectionStart,
|
|
28
|
+
end: next.selectionEnd
|
|
29
|
+
};
|
|
30
|
+
pendingSelectionRef.current = selection;
|
|
31
|
+
lastSelectionRef.current = selection;
|
|
32
|
+
onChange(next.value);
|
|
33
|
+
};
|
|
34
|
+
const rememberSelection = (selection) => {
|
|
35
|
+
lastSelectionRef.current = selection;
|
|
36
|
+
};
|
|
37
|
+
const queueSelection = (selection) => {
|
|
38
|
+
pendingSelectionRef.current = selection;
|
|
39
|
+
lastSelectionRef.current = selection;
|
|
40
|
+
};
|
|
41
|
+
const takeQueuedSelection = () => {
|
|
42
|
+
const selection = pendingSelectionRef.current;
|
|
43
|
+
pendingSelectionRef.current = null;
|
|
44
|
+
return selection;
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
value,
|
|
48
|
+
onChange,
|
|
49
|
+
dispatch,
|
|
50
|
+
rememberSelection,
|
|
51
|
+
queueSelection,
|
|
52
|
+
takeQueuedSelection
|
|
53
|
+
};
|
|
54
|
+
}, [value, onChange]);
|
|
55
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_MarkdownContext.MarkdownContext.Provider, {
|
|
56
|
+
value: context,
|
|
57
|
+
children
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
exports.Control = Control;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/Control/Control.d.ts
|
|
3
|
+
interface ControlProps {
|
|
4
|
+
/** Current markdown source (controlled). */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Called with the next source on edit or a toolbar action. */
|
|
7
|
+
onChange: (value: string) => void;
|
|
8
|
+
/** The composed parts — `Markdown.Toolbar`, `Markdown.Editor`, `Markdown.Preview`, … */
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The headless root of the Markdown compound: owns the shared value/selection state and exposes
|
|
13
|
+
* it (plus a `dispatch` for toolbar actions) via context. It renders no UI of its own — the
|
|
14
|
+
* consumer arranges the parts and decides which panes to show.
|
|
15
|
+
*/
|
|
16
|
+
declare function Control({ value, onChange, children }: Readonly<ControlProps>): import("react").JSX.Element;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { Control, ControlProps };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/Control/Control.d.ts
|
|
3
|
+
interface ControlProps {
|
|
4
|
+
/** Current markdown source (controlled). */
|
|
5
|
+
value: string;
|
|
6
|
+
/** Called with the next source on edit or a toolbar action. */
|
|
7
|
+
onChange: (value: string) => void;
|
|
8
|
+
/** The composed parts — `Markdown.Toolbar`, `Markdown.Editor`, `Markdown.Preview`, … */
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The headless root of the Markdown compound: owns the shared value/selection state and exposes
|
|
13
|
+
* it (plus a `dispatch` for toolbar actions) via context. It renders no UI of its own — the
|
|
14
|
+
* consumer arranges the parts and decides which panes to show.
|
|
15
|
+
*/
|
|
16
|
+
declare function Control({ value, onChange, children }: Readonly<ControlProps>): import("react").JSX.Element;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { Control, ControlProps };
|