next-live 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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/editor.cjs +247 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +80 -0
- package/dist/editor.d.ts +80 -0
- package/dist/editor.js +225 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +1265 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +580 -0
- package/dist/index.d.ts +580 -0
- package/dist/index.js +1192 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +287 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +142 -0
- package/dist/server.d.ts +142 -0
- package/dist/server.js +282 -0
- package/dist/server.js.map +1 -0
- package/dist/shared.js +30 -0
- package/dist/shared.js.map +1 -0
- package/docs/01-getting-started.md +244 -0
- package/docs/02-module-registry.md +487 -0
- package/docs/03-sharing-your-app-libraries.md +206 -0
- package/docs/04-scaling.md +234 -0
- package/docs/05-security.md +204 -0
- package/docs/06-api-reference.md +337 -0
- package/docs/07-troubleshooting.md +289 -0
- package/docs/08-integration-guide.md +340 -0
- package/docs/09-non-ui-snippets.md +124 -0
- package/docs/10-validating-in-ci.md +192 -0
- package/docs/README.md +76 -0
- package/package.json +105 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-09-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Live TSX/JSX evaluation for the Next.js App Router with real ESM `import` statements and a module registry.
|
|
14
|
+
- Three entry points: `next-live` (provider, preview, hooks, engine), `next-live/editor` (syntax-highlighted editor), `next-live/server` (precompile, validateSnippet).
|
|
15
|
+
- `onCodeChange` on `<LiveProvider>`, called when code is edited from inside, not when the `code` prop changes from outside.
|
|
16
|
+
- `precompiledTransform` on the client entry, wrap server-precompiled output without pulling Sucrase into the page bundle.
|
|
17
|
+
- `validateSnippet` / `validateSnippets` for CI validation of stored snippets.
|
|
18
|
+
- `useLiveModule` for non-UI snippets (validators, transformers, config).
|
|
19
|
+
- Render-loop breaker, error boundary, SSR-safe hydration, lazy Sucrase loading.
|
|
20
|
+
- Error-line highlighting in `<LiveEditor>`, compile and runtime errors mark the reported line in the built-in editor; `LiveEditorRenderProps` exposes `error`, `errorLine`, and `errorColumn` for custom editors.
|
|
21
|
+
- `onCompileSuccess` on `<LiveProvider>` / `useLiveRunner` / `useLiveModule`, fires after a successful compile with `compileId`, sorted `imports`, optional `via`, and `durationMs`.
|
|
22
|
+
- `CompileResult` and `CompileModuleResult` now include sorted `imports`.
|
|
23
|
+
- Opt-in stricter `validateSnippet` options: `maxSourceBytes`, `forbidNodeBuiltins`, `forbidRemoteImports`, `denySpecifiers`.
|
|
24
|
+
- `errorPosition` and `PositionedError` exported from `next-live`.
|
|
25
|
+
- `resolveSubpaths` on `<LiveProvider>` / `useLiveRunner` / `useLiveModule`, opt in to resolving `pkg/Sub` against a registered `pkg` by property access.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **`<LiveEditor>` is exported from `next-live/editor`, not `next-live`.** Preview-only pages that never edit snippets should import only from `next-live`, measured ~16 KB instead of ~97 KB when Prism is not needed.
|
|
30
|
+
- `prism-react-renderer` is an **optional** peer dependency. Package managers do not install it automatically, so run `npm install prism-react-renderer` yourself if you import `next-live/editor`. Preview-only pages need nothing extra.
|
|
31
|
+
|
|
32
|
+
[unreleased]: https://github.com/khaledOghli/next-live/compare/v0.1.0...HEAD
|
|
33
|
+
[0.1.0]: https://github.com/khaledOghli/next-live/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Khaled Oghli
|
|
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,142 @@
|
|
|
1
|
+
# next-live
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/next-live)
|
|
4
|
+
[](https://github.com/khaledOghli/next-live/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/next-live)
|
|
6
|
+
[](https://bundlephobia.com/package/next-live)
|
|
7
|
+
[](https://github.com/khaledOghli/next-live/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
Live TSX/JSX evaluation for **React**, real ESM `import` statements, a module
|
|
10
|
+
registry instead of a global scope bag, and no hydration mismatches.
|
|
11
|
+
|
|
12
|
+
**Next.js is not required.** It works in Vite, Remix, CRA, or anywhere React
|
|
13
|
+
runs. The name reflects where it was designed and what it is tuned for: App
|
|
14
|
+
Router SSR safety, and docs written against Next 16.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install next-live
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Using the built-in editor? It needs one optional peer, which npm does **not**
|
|
21
|
+
install for you:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install next-live prism-react-renderer
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Preview-only pages never import `next-live/editor`, and should skip it, that is
|
|
28
|
+
the whole point of keeping the highlighter on a separate entry.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
'use client';
|
|
34
|
+
|
|
35
|
+
import { LiveProvider, LivePreview, LiveError } from 'next-live';
|
|
36
|
+
import { LiveEditor } from 'next-live/editor';
|
|
37
|
+
|
|
38
|
+
export function Playground({ source }: { source: string }) {
|
|
39
|
+
return (
|
|
40
|
+
<LiveProvider code={source}>
|
|
41
|
+
<LiveEditor />
|
|
42
|
+
<LivePreview />
|
|
43
|
+
<LiveError />
|
|
44
|
+
</LiveProvider>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The snippet is written the way a real file is written:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { useState } from 'react';
|
|
53
|
+
|
|
54
|
+
export default function App() {
|
|
55
|
+
const [count, setCount] = useState(0);
|
|
56
|
+
return <button onClick={() => setCount(count + 1)}>clicked {count} times</button>;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
To let snippets reach your own code, hand it over explicitly:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<LiveProvider
|
|
64
|
+
code={source}
|
|
65
|
+
modules={{ '@app/store': defineLoader(() => import('@/lib/store')) }}
|
|
66
|
+
props={{ user }}
|
|
67
|
+
/>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## When to use something else
|
|
71
|
+
|
|
72
|
+
Sandpack solves a different problem: it boots a virtual filesystem and an iframe
|
|
73
|
+
per instance. `next-live` is for embedded live tools and control panels, where a
|
|
74
|
+
snippet should share your page's React instance and your live objects.
|
|
75
|
+
|
|
76
|
+
## What you get
|
|
77
|
+
|
|
78
|
+
- **Real ESM.** `import`, `export default`, namespace imports, deep subpaths.
|
|
79
|
+
- **TypeScript and JSX**, transpiled by Sucrase in single-digit milliseconds.
|
|
80
|
+
- **A module registry** - hand snippets your store, your UI kit, your helpers.
|
|
81
|
+
- **Live props by reference.** Pass a store or a class instance; a snippet
|
|
82
|
+
mutating it updates your app.
|
|
83
|
+
- **SSR-safe.** No hydration mismatch, no `next/dynamic` needed.
|
|
84
|
+
- **Contained failures.** An error boundary and a render-loop breaker keep a bad
|
|
85
|
+
snippet from taking down the page.
|
|
86
|
+
- **Not just components.** `useLiveModule` runs snippets that export
|
|
87
|
+
validators, transformers, or config rather than UI.
|
|
88
|
+
- **CI validation.** `validateSnippets` checks every stored snippet still
|
|
89
|
+
compiles against your registry, so an SDK rename fails the build instead of
|
|
90
|
+
breaking apps silently.
|
|
91
|
+
- **Small, and lazy.** A page that only runs snippets pays **16.1 KB**; the
|
|
92
|
+
editor and its highlighter are a separate entry, and the transpiler is a
|
|
93
|
+
chunk fetched on first compile.
|
|
94
|
+
- **Headless if you want it.** `useLiveRunner` for a completely custom UI.
|
|
95
|
+
- **Server precompilation** via `next-live/server`, so the browser can skip the
|
|
96
|
+
transpiler entirely.
|
|
97
|
+
|
|
98
|
+
## Documentation
|
|
99
|
+
|
|
100
|
+
**[→ Full documentation](./docs/README.md)**
|
|
101
|
+
|
|
102
|
+
| | |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| [Getting started](./docs/01-getting-started.md) | Install and first working preview |
|
|
105
|
+
| [The module registry](./docs/02-module-registry.md) | How `import` resolves, the core concept |
|
|
106
|
+
| [Sharing libraries with your app](./docs/03-sharing-your-app-libraries.md) | One instance, not two copies |
|
|
107
|
+
| [Scaling to many apps](./docs/04-scaling.md) | Keeping the bundle small |
|
|
108
|
+
| [Security](./docs/05-security.md) | Trust model and CSP, read before deploying |
|
|
109
|
+
| [API reference](./docs/06-api-reference.md) | Every export and prop |
|
|
110
|
+
| [Troubleshooting](./docs/07-troubleshooting.md) | Real errors and their fixes |
|
|
111
|
+
| [Integration guide](./docs/08-integration-guide.md) | End-to-end walkthrough |
|
|
112
|
+
| [Snippets that are not components](./docs/09-non-ui-snippets.md) | Validators, transformers, config |
|
|
113
|
+
| [Validating stored snippets in CI](./docs/10-validating-in-ci.md) | Catch SDK renames before users do |
|
|
114
|
+
|
|
115
|
+
## Two things to know up front
|
|
116
|
+
|
|
117
|
+
**It needs `'unsafe-eval'` in your CSP**, scoped to the routes that run snippets.
|
|
118
|
+
That is inherent to compiling at runtime. [Security](./docs/05-security.md)
|
|
119
|
+
explains why it is narrower than it sounds and how to contain it.
|
|
120
|
+
|
|
121
|
+
**It is not a sandbox.** Snippets run with your page's authority. That is what
|
|
122
|
+
makes shared stores and live props work, and it means snippet authors must be
|
|
123
|
+
people you trust.
|
|
124
|
+
|
|
125
|
+
## Requirements
|
|
126
|
+
|
|
127
|
+
React 19+ and Node 20.9+. Nothing else is required: the library imports only
|
|
128
|
+
`react`, `react/jsx-runtime`, `react/jsx-dev-runtime`, `sucrase`, and
|
|
129
|
+
`prism-react-renderer` on the `/editor` entry.
|
|
130
|
+
|
|
131
|
+
## Contributing
|
|
132
|
+
|
|
133
|
+
Bug reports, feature requests and pull requests are welcome. Start with
|
|
134
|
+
[CONTRIBUTING.md](https://github.com/khaledOghli/next-live/blob/main/CONTRIBUTING.md),
|
|
135
|
+
and see the [changelog](./CHANGELOG.md) for what has changed.
|
|
136
|
+
|
|
137
|
+
Found a security issue? Do not open an issue, follow
|
|
138
|
+
[SECURITY.md](https://github.com/khaledOghli/next-live/blob/main/SECURITY.md).
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
[MIT](https://github.com/khaledOghli/next-live/blob/main/LICENSE) (c) KhaledOghli
|
package/dist/editor.cjs
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var prismReactRenderer = require('prism-react-renderer');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/core/positions.ts
|
|
9
|
+
function errorPosition(error) {
|
|
10
|
+
if (error === null || error === void 0) return null;
|
|
11
|
+
const positioned = error;
|
|
12
|
+
if (positioned.line === void 0 || positioned.line < 1) return null;
|
|
13
|
+
return {
|
|
14
|
+
line: positioned.line,
|
|
15
|
+
...positioned.column !== void 0 ? { column: positioned.column } : {}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
var CONTEXT_KEY = /* @__PURE__ */ Symbol.for("next-live.LiveContext");
|
|
19
|
+
function createLiveContext() {
|
|
20
|
+
const store = globalThis;
|
|
21
|
+
const existing = store[CONTEXT_KEY];
|
|
22
|
+
if (existing) return existing;
|
|
23
|
+
const context = react.createContext(null);
|
|
24
|
+
context.displayName = "LiveContext";
|
|
25
|
+
store[CONTEXT_KEY] = context;
|
|
26
|
+
return context;
|
|
27
|
+
}
|
|
28
|
+
var LiveContext = createLiveContext();
|
|
29
|
+
var DEFAULT_ERROR_LINE_STYLE = {
|
|
30
|
+
background: "rgba(179, 38, 30, 0.35)",
|
|
31
|
+
boxShadow: "inset 3px 0 0 #b3261e"
|
|
32
|
+
};
|
|
33
|
+
var DEFAULT_FOCUS_RING_STYLE = {
|
|
34
|
+
outline: "2px solid #4d9fff",
|
|
35
|
+
outlineOffset: "2px"
|
|
36
|
+
};
|
|
37
|
+
var SHARED_TEXT_STYLE = {
|
|
38
|
+
margin: 0,
|
|
39
|
+
border: 0,
|
|
40
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
|
|
41
|
+
fontSize: "0.8125rem",
|
|
42
|
+
lineHeight: 1.6,
|
|
43
|
+
fontVariantLigatures: "none",
|
|
44
|
+
tabSize: 2,
|
|
45
|
+
whiteSpace: "pre",
|
|
46
|
+
wordBreak: "normal",
|
|
47
|
+
overflowWrap: "normal"
|
|
48
|
+
};
|
|
49
|
+
var VISUALLY_HIDDEN = {
|
|
50
|
+
position: "absolute",
|
|
51
|
+
width: 1,
|
|
52
|
+
height: 1,
|
|
53
|
+
padding: 0,
|
|
54
|
+
margin: -1,
|
|
55
|
+
overflow: "hidden",
|
|
56
|
+
clip: "rect(0 0 0 0)",
|
|
57
|
+
whiteSpace: "nowrap",
|
|
58
|
+
border: 0
|
|
59
|
+
};
|
|
60
|
+
function prefersVisibleFocus(element) {
|
|
61
|
+
try {
|
|
62
|
+
return element.matches(":focus-visible");
|
|
63
|
+
} catch {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function LiveEditor(props) {
|
|
68
|
+
const {
|
|
69
|
+
className,
|
|
70
|
+
style,
|
|
71
|
+
theme = prismReactRenderer.themes.vsDark,
|
|
72
|
+
readOnly: readOnlyProp,
|
|
73
|
+
tabSize = 2,
|
|
74
|
+
padding = 16,
|
|
75
|
+
errorLineStyle = DEFAULT_ERROR_LINE_STYLE,
|
|
76
|
+
errorLineClassName,
|
|
77
|
+
code: codeProp,
|
|
78
|
+
onChange: onChangeProp,
|
|
79
|
+
language: languageProp,
|
|
80
|
+
error: errorProp,
|
|
81
|
+
prism,
|
|
82
|
+
focusRingStyle = DEFAULT_FOCUS_RING_STYLE,
|
|
83
|
+
"aria-label": ariaLabel = "Live code editor",
|
|
84
|
+
renderEditor
|
|
85
|
+
} = props;
|
|
86
|
+
const live = react.useContext(LiveContext);
|
|
87
|
+
const highlightRef = react.useRef(null);
|
|
88
|
+
const hintId = react.useId();
|
|
89
|
+
const [tabEscapes, setTabEscapes] = react.useState(false);
|
|
90
|
+
const [focusRing, setFocusRing] = react.useState(false);
|
|
91
|
+
if (codeProp === void 0 && live === null) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"<LiveEditor> needs either a surrounding <LiveProvider> or a `code` prop. Pass `code` (and `onChange` to make it editable) to use it standalone."
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const code = codeProp ?? live?.code ?? "";
|
|
97
|
+
const language = languageProp ?? live?.language ?? "tsx";
|
|
98
|
+
const error = errorProp !== void 0 ? errorProp : live?.error ?? null;
|
|
99
|
+
const setCode = onChangeProp ?? (codeProp === void 0 ? live?.setCode : void 0);
|
|
100
|
+
const readOnly = readOnlyProp ?? setCode === void 0;
|
|
101
|
+
const position = error ? errorPosition(error) : null;
|
|
102
|
+
const errorLine = position?.line;
|
|
103
|
+
const errorColumn = position?.column;
|
|
104
|
+
const handleChange = react.useCallback(
|
|
105
|
+
(value) => {
|
|
106
|
+
setCode?.(value);
|
|
107
|
+
},
|
|
108
|
+
[setCode]
|
|
109
|
+
);
|
|
110
|
+
const renderProps = {
|
|
111
|
+
code,
|
|
112
|
+
onChange: handleChange,
|
|
113
|
+
language,
|
|
114
|
+
error,
|
|
115
|
+
errorLine,
|
|
116
|
+
errorColumn
|
|
117
|
+
};
|
|
118
|
+
if (renderEditor) {
|
|
119
|
+
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: renderEditor(renderProps) });
|
|
120
|
+
}
|
|
121
|
+
const syncScroll = (event) => {
|
|
122
|
+
const node = highlightRef.current;
|
|
123
|
+
if (!node) return;
|
|
124
|
+
node.scrollTop = event.currentTarget.scrollTop;
|
|
125
|
+
node.scrollLeft = event.currentTarget.scrollLeft;
|
|
126
|
+
};
|
|
127
|
+
const handleKeyDown = (event) => {
|
|
128
|
+
if (event.key === "Escape") {
|
|
129
|
+
setTabEscapes(true);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (event.key !== "Tab") {
|
|
133
|
+
if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
|
|
134
|
+
if (tabEscapes) setTabEscapes(false);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (tabEscapes || readOnly) {
|
|
139
|
+
setTabEscapes(false);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
const target = event.currentTarget;
|
|
144
|
+
const { selectionStart, selectionEnd, value } = target;
|
|
145
|
+
const indent = " ".repeat(tabSize);
|
|
146
|
+
const next = value.slice(0, selectionStart) + indent + value.slice(selectionEnd);
|
|
147
|
+
handleChange(next);
|
|
148
|
+
requestAnimationFrame(() => {
|
|
149
|
+
target.selectionStart = target.selectionEnd = selectionStart + indent.length;
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
const layerStyle = {
|
|
153
|
+
...SHARED_TEXT_STYLE,
|
|
154
|
+
tabSize,
|
|
155
|
+
padding,
|
|
156
|
+
gridArea: "1 / 1",
|
|
157
|
+
minWidth: 0,
|
|
158
|
+
minHeight: 0,
|
|
159
|
+
overflow: "auto"
|
|
160
|
+
};
|
|
161
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
162
|
+
"div",
|
|
163
|
+
{
|
|
164
|
+
className,
|
|
165
|
+
style: {
|
|
166
|
+
// A single-cell grid makes both layers size to the larger of the two,
|
|
167
|
+
// which absolute positioning cannot do without a fixed height.
|
|
168
|
+
display: "grid",
|
|
169
|
+
position: "relative",
|
|
170
|
+
background: theme.plain.backgroundColor ?? "#1e1e1e",
|
|
171
|
+
...style,
|
|
172
|
+
// The ring is painted on the wrapper rather than the textarea, whose
|
|
173
|
+
// own outline would be clipped by its scroll box. Applied after the
|
|
174
|
+
// caller's `style` so it is not accidentally overwritten.
|
|
175
|
+
...focusRing && focusRingStyle ? focusRingStyle : null
|
|
176
|
+
},
|
|
177
|
+
children: [
|
|
178
|
+
/* @__PURE__ */ jsxRuntime.jsx(prismReactRenderer.Highlight, { prism, code, language, theme, children: ({ style: prismStyle, tokens, getLineProps, getTokenProps }) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
179
|
+
"pre",
|
|
180
|
+
{
|
|
181
|
+
ref: highlightRef,
|
|
182
|
+
"aria-hidden": "true",
|
|
183
|
+
style: { ...prismStyle, ...layerStyle },
|
|
184
|
+
children: tokens.map((line, i) => {
|
|
185
|
+
const isErrorLine = errorLineStyle !== null && errorLine === i + 1;
|
|
186
|
+
const lineProps = getLineProps({ line });
|
|
187
|
+
return (
|
|
188
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
189
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
190
|
+
"div",
|
|
191
|
+
{
|
|
192
|
+
...lineProps,
|
|
193
|
+
className: [lineProps.className, isErrorLine ? errorLineClassName : void 0].filter(Boolean).join(" "),
|
|
194
|
+
style: isErrorLine ? { ...lineProps.style, ...errorLineStyle } : lineProps.style,
|
|
195
|
+
children: line.map((token, key) => (
|
|
196
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
197
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { ...getTokenProps({ token }) }, key)
|
|
198
|
+
))
|
|
199
|
+
},
|
|
200
|
+
i
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
) }),
|
|
206
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
207
|
+
"textarea",
|
|
208
|
+
{
|
|
209
|
+
value: code,
|
|
210
|
+
onChange: (event) => handleChange(event.target.value),
|
|
211
|
+
onScroll: syncScroll,
|
|
212
|
+
onKeyDown: handleKeyDown,
|
|
213
|
+
onFocus: (event) => setFocusRing(prefersVisibleFocus(event.currentTarget)),
|
|
214
|
+
onBlur: () => {
|
|
215
|
+
setFocusRing(false);
|
|
216
|
+
setTabEscapes(false);
|
|
217
|
+
},
|
|
218
|
+
readOnly,
|
|
219
|
+
spellCheck: false,
|
|
220
|
+
autoCapitalize: "off",
|
|
221
|
+
autoCorrect: "off",
|
|
222
|
+
autoComplete: "off",
|
|
223
|
+
"aria-label": ariaLabel,
|
|
224
|
+
"aria-describedby": readOnly ? void 0 : hintId,
|
|
225
|
+
"aria-keyshortcuts": readOnly ? void 0 : "Escape",
|
|
226
|
+
style: {
|
|
227
|
+
...layerStyle,
|
|
228
|
+
resize: "none",
|
|
229
|
+
// The wrapper paints the ring instead - see above.
|
|
230
|
+
outline: "none",
|
|
231
|
+
background: "transparent",
|
|
232
|
+
// The text itself is invisible; only the highlighted layer is read.
|
|
233
|
+
// The caret is painted separately so it stays visible.
|
|
234
|
+
color: "transparent",
|
|
235
|
+
caretColor: theme.plain.color ?? "#fff"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
),
|
|
239
|
+
!readOnly && /* @__PURE__ */ jsxRuntime.jsx("span", { id: hintId, style: VISUALLY_HIDDEN, children: "Tab inserts spaces. Press Escape and then Tab to move focus out of the editor." })
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
exports.LiveEditor = LiveEditor;
|
|
246
|
+
//# sourceMappingURL=editor.cjs.map
|
|
247
|
+
//# sourceMappingURL=editor.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/positions.ts","../src/context/LiveContext.tsx","../src/components/LiveEditor.tsx"],"names":["createContext","themes","useContext","useRef","useId","useState","useCallback","jsx","Fragment","jsxs","Highlight"],"mappings":"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","file":"editor.cjs","sourcesContent":["export interface PositionedError extends Error {\n line?: number;\n column?: number;\n}\n\n/**\n * Reads a 1-based line/column from a compile or runtime error, if present.\n *\n * Accepts a nullish value and returns null for it. The state this is meant to\n * be fed from - `LiveRunnerState.error` and `LiveContextValue.error` - is\n * `Error | null`, so `errorPosition(error)` is the natural thing to write in a\n * custom editor. TypeScript rejects that, but a JavaScript host would have hit\n * a TypeError instead of the \"no position\" answer the name promises.\n */\nexport function errorPosition(error: Error | null | undefined): { line: number; column?: number } | null {\n if (error === null || error === undefined) return null;\n const positioned = error as PositionedError;\n if (positioned.line === undefined || positioned.line < 1) return null;\n return {\n line: positioned.line,\n ...(positioned.column !== undefined ? { column: positioned.column } : {}),\n };\n}\n","'use client';\n\nimport { createContext } from 'react';\nimport type { Context } from 'react';\nimport type { LiveContextValue } from '../core/types';\n\n/**\n * One context, however many copies of this module exist.\n *\n * The context is cached on a global symbol rather than being a plain\n * module-level constant, because a plain one is only unique per module\n * instance, and there are realistic ways to end up with more than one:\n *\n * - The CommonJS build cannot code-split, so `index.cjs` and `editor.cjs` each\n * inline their own copy. Without this, `<LiveEditor>` from `next-live/editor`\n * would read a different context than `<LiveProvider>` provides and throw\n * \"useLiveContext must be called inside a <LiveProvider>\".\n * - npm can install two versions of the package side by side.\n *\n * Keying on a symbol means every copy resolves to whichever created it first,\n * so provider and consumer always meet.\n */\nconst CONTEXT_KEY = Symbol.for('next-live.LiveContext');\n\ntype GlobalWithContext = typeof globalThis & {\n [CONTEXT_KEY]?: Context<LiveContextValue | null>;\n};\n\nfunction createLiveContext(): Context<LiveContextValue | null> {\n const store = globalThis as GlobalWithContext;\n const existing = store[CONTEXT_KEY];\n if (existing) return existing;\n\n /**\n * Null rather than a default value, so `useLiveContext` can tell \"outside a\n * provider\" apart from \"inside a provider that has not compiled yet\".\n */\n const context = createContext<LiveContextValue | null>(null);\n context.displayName = 'LiveContext';\n store[CONTEXT_KEY] = context;\n return context;\n}\n\nexport const LiveContext = createLiveContext();\n","'use client';\n\nimport { useCallback, useContext, useId, useRef, useState } from 'react';\nimport type { CSSProperties, ReactNode } from 'react';\nimport { Highlight, themes } from 'prism-react-renderer';\nimport type { Prism, PrismTheme } from 'prism-react-renderer';\nimport { errorPosition } from '../core/positions';\nimport { LiveContext } from '../context/LiveContext';\n\nexport interface LiveEditorRenderProps {\n code: string;\n onChange: (code: string) => void;\n language: string;\n error: Error | null;\n errorLine?: number;\n errorColumn?: number;\n}\n\nexport interface LiveEditorProps {\n className?: string;\n style?: CSSProperties;\n theme?: PrismTheme;\n /** Read-only mode, for showing an app's source without allowing edits. */\n readOnly?: boolean;\n /** Spaces inserted by the Tab key. Default 2. */\n tabSize?: number;\n padding?: number;\n /** Paint-only highlight for the error line. Pass `null` to disable. */\n errorLineStyle?: CSSProperties | null;\n errorLineClassName?: string;\n /**\n * Standalone mode: the code to show, when there is no `<LiveProvider>`\n * above. Inside a provider this overrides the provider's code, which is\n * rarely what you want - omit it and let the context supply it.\n */\n code?: string;\n /**\n * Standalone mode: notified on every edit. Without it - and without a\n * provider to call `setCode` on - there is nothing to write an edit to, so\n * the editor renders read-only.\n */\n onChange?: (code: string) => void;\n /** Highlighting language. Defaults to the provider's, then `'tsx'`. */\n language?: string;\n /** Error to underline. Defaults to the provider's. */\n error?: Error | null;\n /**\n * A Prism instance with extra languages registered, for highlighting\n * anything outside `prism-react-renderer`'s built-in set.\n *\n * ```ts\n * import { Prism } from 'prism-react-renderer';\n * (globalThis as any).Prism = Prism;\n * await import('prismjs/components/prism-rust');\n * ```\n */\n prism?: typeof Prism;\n /**\n * Ring painted around the editor while it holds keyboard focus. Pass `null`\n * to disable - but then paint your own, or keyboard users cannot see where\n * they are (WCAG 2.4.7).\n */\n focusRingStyle?: CSSProperties | null;\n /** Overrides the default accessible name. */\n 'aria-label'?: string;\n /**\n * Replaces the built-in editor entirely - drop in CodeMirror, Monaco, or\n * anything else while keeping the rest of the provider wiring.\n */\n renderEditor?: (props: LiveEditorRenderProps) => ReactNode;\n}\n\nconst DEFAULT_ERROR_LINE_STYLE: CSSProperties = {\n background: 'rgba(179, 38, 30, 0.35)',\n boxShadow: 'inset 3px 0 0 #b3261e',\n};\n\n/**\n * Two pixels of high-contrast outline, offset so it reads against both the\n * editor background and the page behind it.\n */\nconst DEFAULT_FOCUS_RING_STYLE: CSSProperties = {\n outline: '2px solid #4d9fff',\n outlineOffset: '2px',\n};\n\n/**\n * Typography shared by the textarea and the highlighted layer beneath it.\n *\n * Every value here has to match in both layers or the visible text will drift\n * out of alignment with the caret - that is the whole difficulty of this\n * technique, so the styles live in one object rather than being repeated.\n */\nconst SHARED_TEXT_STYLE: CSSProperties = {\n margin: 0,\n border: 0,\n fontFamily:\n 'ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace',\n fontSize: '0.8125rem',\n lineHeight: 1.6,\n fontVariantLigatures: 'none',\n tabSize: 2,\n whiteSpace: 'pre',\n wordBreak: 'normal',\n overflowWrap: 'normal',\n};\n\n/** Hides the focus hint from sight while leaving it on the accessibility tree. */\nconst VISUALLY_HIDDEN: CSSProperties = {\n position: 'absolute',\n width: 1,\n height: 1,\n padding: 0,\n margin: -1,\n overflow: 'hidden',\n clip: 'rect(0 0 0 0)',\n whiteSpace: 'nowrap',\n border: 0,\n};\n\n/**\n * `:focus-visible` is what decides whether a focus ring should be painted, but\n * it can only be asked of a live element - and jsdom, plus a few older\n * engines, do not implement it. Treating an unanswerable question as \"yes\"\n * fails safe: a ring that shows after a mouse click is a cosmetic complaint,\n * a ring that never shows is an accessibility defect.\n */\nfunction prefersVisibleFocus(element: HTMLElement): boolean {\n try {\n return element.matches(':focus-visible');\n } catch {\n return true;\n }\n}\n\n/**\n * A code editor built from a transparent `<textarea>` layered over syntax\n * highlighted output.\n *\n * There is no editor engine here by design - that keeps the bundle small and\n * sidesteps the SSR problems full editors bring. When a real editor is wanted,\n * pass `renderEditor`.\n *\n * Usually rendered inside a `<LiveProvider>`, which supplies the code and\n * receives the edits. It also works standalone - pass `code`, and `onChange`\n * if it should be editable - which is how a page shows a highlighted,\n * read-only snippet without spinning up a provider to run it.\n */\nexport function LiveEditor(props: LiveEditorProps): ReactNode {\n const {\n className,\n style,\n theme = themes.vsDark,\n readOnly: readOnlyProp,\n tabSize = 2,\n padding = 16,\n errorLineStyle = DEFAULT_ERROR_LINE_STYLE,\n errorLineClassName,\n code: codeProp,\n onChange: onChangeProp,\n language: languageProp,\n error: errorProp,\n prism,\n focusRingStyle = DEFAULT_FOCUS_RING_STYLE,\n 'aria-label': ariaLabel = 'Live code editor',\n renderEditor,\n } = props;\n\n // Read the context directly rather than through `useLiveContext`, which\n // throws when there is no provider. Standalone rendering is supported, so\n // \"no provider\" is a valid state here rather than a mistake.\n const live = useContext(LiveContext);\n const highlightRef = useRef<HTMLPreElement>(null);\n const hintId = useId();\n\n /**\n * Set by Escape, cleared by anything else. While set, Tab moves focus\n * instead of indenting.\n *\n * A code editor has to swallow Tab to be usable, and swallowing Tab with no\n * way out is a keyboard trap - WCAG 2.1.2, a Level A failure. Escape-then-Tab\n * is the convention CodeMirror and Monaco use, and `aria-describedby` below\n * announces it, which 2.1.2 requires whenever the escape is non-obvious.\n */\n const [tabEscapes, setTabEscapes] = useState(false);\n const [focusRing, setFocusRing] = useState(false);\n\n if (codeProp === undefined && live === null) {\n throw new Error(\n '<LiveEditor> needs either a surrounding <LiveProvider> or a `code` prop. ' +\n 'Pass `code` (and `onChange` to make it editable) to use it standalone.',\n );\n }\n\n const code = codeProp ?? live?.code ?? '';\n const language = languageProp ?? live?.language ?? 'tsx';\n const error = errorProp !== undefined ? errorProp : (live?.error ?? null);\n\n // An editor with nowhere to send an edit is read-only whether it says so or\n // not; making that explicit keeps the textarea's behaviour honest and stops\n // keystrokes being silently swallowed.\n const setCode = onChangeProp ?? (codeProp === undefined ? live?.setCode : undefined);\n const readOnly = readOnlyProp ?? setCode === undefined;\n\n const position = error ? errorPosition(error) : null;\n const errorLine = position?.line;\n const errorColumn = position?.column;\n\n const handleChange = useCallback(\n (value: string) => {\n setCode?.(value);\n },\n [setCode],\n );\n\n const renderProps: LiveEditorRenderProps = {\n code,\n onChange: handleChange,\n language,\n error,\n errorLine,\n errorColumn,\n };\n\n if (renderEditor) {\n return <>{renderEditor(renderProps)}</>;\n }\n\n // The highlighted layer does not scroll on its own - it is not focusable -\n // so it has to be driven from the textarea to stay aligned.\n const syncScroll = (event: React.UIEvent<HTMLTextAreaElement>) => {\n const node = highlightRef.current;\n if (!node) return;\n node.scrollTop = event.currentTarget.scrollTop;\n node.scrollLeft = event.currentTarget.scrollLeft;\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (event.key === 'Escape') {\n // Arms the exit. Nothing is prevented: Escape has no meaning in a\n // textarea, but a dialog wrapping the editor may well want it.\n setTabEscapes(true);\n return;\n }\n\n if (event.key !== 'Tab') {\n // Any other key means the author kept typing, so re-arm indentation.\n // Modifier presses on their own are not \"typing\" and must not disarm the\n // exit - Shift+Tab fires a Shift keydown first, and disarming there\n // would make backwards escape impossible.\n if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {\n if (tabEscapes) setTabEscapes(false);\n }\n return;\n }\n\n // Tab. Let it through when armed, or when there is nothing to edit.\n if (tabEscapes || readOnly) {\n setTabEscapes(false);\n return;\n }\n\n event.preventDefault();\n const target = event.currentTarget;\n const { selectionStart, selectionEnd, value } = target;\n const indent = ' '.repeat(tabSize);\n const next = value.slice(0, selectionStart) + indent + value.slice(selectionEnd);\n handleChange(next);\n requestAnimationFrame(() => {\n target.selectionStart = target.selectionEnd = selectionStart + indent.length;\n });\n };\n\n const layerStyle: CSSProperties = {\n ...SHARED_TEXT_STYLE,\n tabSize,\n padding,\n gridArea: '1 / 1',\n minWidth: 0,\n minHeight: 0,\n overflow: 'auto',\n };\n\n return (\n <div\n className={className}\n style={{\n // A single-cell grid makes both layers size to the larger of the two,\n // which absolute positioning cannot do without a fixed height.\n display: 'grid',\n position: 'relative',\n background: theme.plain.backgroundColor ?? '#1e1e1e',\n ...style,\n // The ring is painted on the wrapper rather than the textarea, whose\n // own outline would be clipped by its scroll box. Applied after the\n // caller's `style` so it is not accidentally overwritten.\n ...(focusRing && focusRingStyle ? focusRingStyle : null),\n }}\n >\n <Highlight prism={prism} code={code} language={language} theme={theme}>\n {({ style: prismStyle, tokens, getLineProps, getTokenProps }) => (\n <pre\n ref={highlightRef}\n aria-hidden=\"true\"\n style={{ ...prismStyle, ...layerStyle }}\n >\n {tokens.map((line, i) => {\n const isErrorLine = errorLineStyle !== null && errorLine === i + 1;\n const lineProps = getLineProps({ line });\n return (\n // eslint-disable-next-line react/no-array-index-key\n <div\n key={i}\n {...lineProps}\n className={[lineProps.className, isErrorLine ? errorLineClassName : undefined]\n .filter(Boolean)\n .join(' ')}\n style={\n isErrorLine\n ? { ...lineProps.style, ...errorLineStyle }\n : lineProps.style\n }\n >\n {line.map((token, key) => (\n // eslint-disable-next-line react/no-array-index-key\n <span key={key} {...getTokenProps({ token })} />\n ))}\n </div>\n );\n })}\n </pre>\n )}\n </Highlight>\n\n <textarea\n value={code}\n onChange={(event) => handleChange(event.target.value)}\n onScroll={syncScroll}\n onKeyDown={handleKeyDown}\n onFocus={(event) => setFocusRing(prefersVisibleFocus(event.currentTarget))}\n onBlur={() => {\n setFocusRing(false);\n // Leaving and returning should not silently keep the exit armed.\n setTabEscapes(false);\n }}\n readOnly={readOnly}\n spellCheck={false}\n autoCapitalize=\"off\"\n autoCorrect=\"off\"\n autoComplete=\"off\"\n aria-label={ariaLabel}\n aria-describedby={readOnly ? undefined : hintId}\n aria-keyshortcuts={readOnly ? undefined : 'Escape'}\n style={{\n ...layerStyle,\n resize: 'none',\n // The wrapper paints the ring instead - see above.\n outline: 'none',\n background: 'transparent',\n // The text itself is invisible; only the highlighted layer is read.\n // The caret is painted separately so it stays visible.\n color: 'transparent',\n caretColor: theme.plain.color ?? '#fff',\n }}\n />\n\n {!readOnly && (\n <span id={hintId} style={VISUALLY_HIDDEN}>\n Tab inserts spaces. Press Escape and then Tab to move focus out of the\n editor.\n </span>\n )}\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { PrismTheme, Prism } from 'prism-react-renderer';
|
|
3
|
+
|
|
4
|
+
interface LiveEditorRenderProps {
|
|
5
|
+
code: string;
|
|
6
|
+
onChange: (code: string) => void;
|
|
7
|
+
language: string;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
errorLine?: number;
|
|
10
|
+
errorColumn?: number;
|
|
11
|
+
}
|
|
12
|
+
interface LiveEditorProps {
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
theme?: PrismTheme;
|
|
16
|
+
/** Read-only mode, for showing an app's source without allowing edits. */
|
|
17
|
+
readOnly?: boolean;
|
|
18
|
+
/** Spaces inserted by the Tab key. Default 2. */
|
|
19
|
+
tabSize?: number;
|
|
20
|
+
padding?: number;
|
|
21
|
+
/** Paint-only highlight for the error line. Pass `null` to disable. */
|
|
22
|
+
errorLineStyle?: CSSProperties | null;
|
|
23
|
+
errorLineClassName?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Standalone mode: the code to show, when there is no `<LiveProvider>`
|
|
26
|
+
* above. Inside a provider this overrides the provider's code, which is
|
|
27
|
+
* rarely what you want - omit it and let the context supply it.
|
|
28
|
+
*/
|
|
29
|
+
code?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Standalone mode: notified on every edit. Without it - and without a
|
|
32
|
+
* provider to call `setCode` on - there is nothing to write an edit to, so
|
|
33
|
+
* the editor renders read-only.
|
|
34
|
+
*/
|
|
35
|
+
onChange?: (code: string) => void;
|
|
36
|
+
/** Highlighting language. Defaults to the provider's, then `'tsx'`. */
|
|
37
|
+
language?: string;
|
|
38
|
+
/** Error to underline. Defaults to the provider's. */
|
|
39
|
+
error?: Error | null;
|
|
40
|
+
/**
|
|
41
|
+
* A Prism instance with extra languages registered, for highlighting
|
|
42
|
+
* anything outside `prism-react-renderer`'s built-in set.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { Prism } from 'prism-react-renderer';
|
|
46
|
+
* (globalThis as any).Prism = Prism;
|
|
47
|
+
* await import('prismjs/components/prism-rust');
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
prism?: typeof Prism;
|
|
51
|
+
/**
|
|
52
|
+
* Ring painted around the editor while it holds keyboard focus. Pass `null`
|
|
53
|
+
* to disable - but then paint your own, or keyboard users cannot see where
|
|
54
|
+
* they are (WCAG 2.4.7).
|
|
55
|
+
*/
|
|
56
|
+
focusRingStyle?: CSSProperties | null;
|
|
57
|
+
/** Overrides the default accessible name. */
|
|
58
|
+
'aria-label'?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Replaces the built-in editor entirely - drop in CodeMirror, Monaco, or
|
|
61
|
+
* anything else while keeping the rest of the provider wiring.
|
|
62
|
+
*/
|
|
63
|
+
renderEditor?: (props: LiveEditorRenderProps) => ReactNode;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A code editor built from a transparent `<textarea>` layered over syntax
|
|
67
|
+
* highlighted output.
|
|
68
|
+
*
|
|
69
|
+
* There is no editor engine here by design - that keeps the bundle small and
|
|
70
|
+
* sidesteps the SSR problems full editors bring. When a real editor is wanted,
|
|
71
|
+
* pass `renderEditor`.
|
|
72
|
+
*
|
|
73
|
+
* Usually rendered inside a `<LiveProvider>`, which supplies the code and
|
|
74
|
+
* receives the edits. It also works standalone - pass `code`, and `onChange`
|
|
75
|
+
* if it should be editable - which is how a page shows a highlighted,
|
|
76
|
+
* read-only snippet without spinning up a provider to run it.
|
|
77
|
+
*/
|
|
78
|
+
declare function LiveEditor(props: LiveEditorProps): ReactNode;
|
|
79
|
+
|
|
80
|
+
export { LiveEditor, type LiveEditorProps, type LiveEditorRenderProps };
|
package/dist/editor.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { PrismTheme, Prism } from 'prism-react-renderer';
|
|
3
|
+
|
|
4
|
+
interface LiveEditorRenderProps {
|
|
5
|
+
code: string;
|
|
6
|
+
onChange: (code: string) => void;
|
|
7
|
+
language: string;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
errorLine?: number;
|
|
10
|
+
errorColumn?: number;
|
|
11
|
+
}
|
|
12
|
+
interface LiveEditorProps {
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
theme?: PrismTheme;
|
|
16
|
+
/** Read-only mode, for showing an app's source without allowing edits. */
|
|
17
|
+
readOnly?: boolean;
|
|
18
|
+
/** Spaces inserted by the Tab key. Default 2. */
|
|
19
|
+
tabSize?: number;
|
|
20
|
+
padding?: number;
|
|
21
|
+
/** Paint-only highlight for the error line. Pass `null` to disable. */
|
|
22
|
+
errorLineStyle?: CSSProperties | null;
|
|
23
|
+
errorLineClassName?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Standalone mode: the code to show, when there is no `<LiveProvider>`
|
|
26
|
+
* above. Inside a provider this overrides the provider's code, which is
|
|
27
|
+
* rarely what you want - omit it and let the context supply it.
|
|
28
|
+
*/
|
|
29
|
+
code?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Standalone mode: notified on every edit. Without it - and without a
|
|
32
|
+
* provider to call `setCode` on - there is nothing to write an edit to, so
|
|
33
|
+
* the editor renders read-only.
|
|
34
|
+
*/
|
|
35
|
+
onChange?: (code: string) => void;
|
|
36
|
+
/** Highlighting language. Defaults to the provider's, then `'tsx'`. */
|
|
37
|
+
language?: string;
|
|
38
|
+
/** Error to underline. Defaults to the provider's. */
|
|
39
|
+
error?: Error | null;
|
|
40
|
+
/**
|
|
41
|
+
* A Prism instance with extra languages registered, for highlighting
|
|
42
|
+
* anything outside `prism-react-renderer`'s built-in set.
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { Prism } from 'prism-react-renderer';
|
|
46
|
+
* (globalThis as any).Prism = Prism;
|
|
47
|
+
* await import('prismjs/components/prism-rust');
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
prism?: typeof Prism;
|
|
51
|
+
/**
|
|
52
|
+
* Ring painted around the editor while it holds keyboard focus. Pass `null`
|
|
53
|
+
* to disable - but then paint your own, or keyboard users cannot see where
|
|
54
|
+
* they are (WCAG 2.4.7).
|
|
55
|
+
*/
|
|
56
|
+
focusRingStyle?: CSSProperties | null;
|
|
57
|
+
/** Overrides the default accessible name. */
|
|
58
|
+
'aria-label'?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Replaces the built-in editor entirely - drop in CodeMirror, Monaco, or
|
|
61
|
+
* anything else while keeping the rest of the provider wiring.
|
|
62
|
+
*/
|
|
63
|
+
renderEditor?: (props: LiveEditorRenderProps) => ReactNode;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A code editor built from a transparent `<textarea>` layered over syntax
|
|
67
|
+
* highlighted output.
|
|
68
|
+
*
|
|
69
|
+
* There is no editor engine here by design - that keeps the bundle small and
|
|
70
|
+
* sidesteps the SSR problems full editors bring. When a real editor is wanted,
|
|
71
|
+
* pass `renderEditor`.
|
|
72
|
+
*
|
|
73
|
+
* Usually rendered inside a `<LiveProvider>`, which supplies the code and
|
|
74
|
+
* receives the edits. It also works standalone - pass `code`, and `onChange`
|
|
75
|
+
* if it should be editable - which is how a page shows a highlighted,
|
|
76
|
+
* read-only snippet without spinning up a provider to run it.
|
|
77
|
+
*/
|
|
78
|
+
declare function LiveEditor(props: LiveEditorProps): ReactNode;
|
|
79
|
+
|
|
80
|
+
export { LiveEditor, type LiveEditorProps, type LiveEditorRenderProps };
|