tosijs-ui 1.6.19 → 1.6.21
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 +18 -0
- package/dist/code-editor-cm.d.ts +48 -0
- package/dist/code-editor-cm.js +219 -0
- package/dist/code-editor.d.ts +35 -9
- package/dist/code-editor.js +90 -62
- package/dist/doc-browser.js +13 -5
- package/dist/doc-system/site/dev-server.js +4 -4
- package/dist/doc-system/site/index.d.ts +1 -1
- package/dist/doc-system/site/make-llms-txt.js +16 -0
- package/dist/doc-system/site/orchestrator.js +41 -5
- package/dist/doc-system/site/site-config.d.ts +28 -0
- package/dist/iife.js +153 -109
- package/dist/iife.js.map +37 -9
- package/dist/live-example/code-transform.js +1 -1
- package/dist/live-example/component.js +192 -14
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +9 -2
- package/package.json +20 -6
- package/tls/create-dev-certs.sh +25 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`import 'tosijs-ui'` no longer resolves to the IIFE under browser bundlers.**
|
|
8
|
+
The `"."` export had a `"browser"` condition pointing at `dist/iife.js` — the
|
|
9
|
+
self-contained CDN/doc-site bundle that inlines tosijs + marked. Browser-targeted
|
|
10
|
+
bundlers (Vite, webpack, esbuild) picked it, so consumers **double-bundled tosijs**
|
|
11
|
+
instead of externalizing the peer, and named imports broke (the IIFE isn't an ESM
|
|
12
|
+
module). Removed the condition: every `import` now resolves to `dist/index.js`
|
|
13
|
+
(ESM, peers externalized). The IIFE stays for CDN `<script>` use and the naive
|
|
14
|
+
doc site — referenced by file path, never via `import 'tosijs-ui'`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **tosijs peer + dev dependency bumped to `^1.6.6`** — picks up a subtle
|
|
19
|
+
component-lifecycle fix.
|
|
20
|
+
|
|
3
21
|
## 1.6.19
|
|
4
22
|
|
|
5
23
|
Doc-system tooling; no component API changes.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { EditorView } from '@codemirror/view';
|
|
2
|
+
import { Extension } from '@codemirror/state';
|
|
3
|
+
/** Map a `<tosi-code mode>` value to a CodeMirror language extension. */
|
|
4
|
+
export declare function languageForMode(mode: string): Extension;
|
|
5
|
+
/** Runtime-introspection hooks for tjs autocomplete (all optional). */
|
|
6
|
+
export interface TjsAutocompleteConfig {
|
|
7
|
+
getMetadata?: () => Record<string, unknown> | undefined;
|
|
8
|
+
getImports?: () => Record<string, Record<string, unknown>> | undefined;
|
|
9
|
+
getLiveBindings?: () => Record<string, unknown> | undefined;
|
|
10
|
+
getMembers?: (path: string) => Promise<unknown[] | undefined>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Lazy-load tjs-lang's CodeMirror language + completion bundle. Returns a single
|
|
14
|
+
* CodeMirror `Extension` (tjs language, forbidden-keyword highlighting, theme, and
|
|
15
|
+
* `tjsCompletionSource`-driven autocomplete) suitable for `setLanguageExtension()`,
|
|
16
|
+
* or `null` when tjs-lang (an optional peer) isn't installed or its editor build
|
|
17
|
+
* lacks `tjsEditorExtension`. The dynamic import keeps it out of the bundle until a
|
|
18
|
+
* tjs editor actually mounts.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadTjsExtension(autocomplete?: TjsAutocompleteConfig): Promise<Extension | null>;
|
|
21
|
+
export interface CmHandle {
|
|
22
|
+
view: EditorView;
|
|
23
|
+
getValue(): string;
|
|
24
|
+
setValue(text: string): void;
|
|
25
|
+
setMode(mode: string): void;
|
|
26
|
+
/** Swap the language compartment wholesale (e.g. an async-loaded tjs bundle). */
|
|
27
|
+
setLanguageExtension(ext: Extension): void;
|
|
28
|
+
setReadOnly(readOnly: boolean): void;
|
|
29
|
+
undo(): void;
|
|
30
|
+
redo(): void;
|
|
31
|
+
canUndo(): boolean;
|
|
32
|
+
canRedo(): boolean;
|
|
33
|
+
focus(): void;
|
|
34
|
+
refresh(): void;
|
|
35
|
+
destroy(): void;
|
|
36
|
+
}
|
|
37
|
+
export interface CmOptions {
|
|
38
|
+
value?: string;
|
|
39
|
+
mode?: string;
|
|
40
|
+
readOnly?: boolean;
|
|
41
|
+
/** additional language-adjacent extensions (e.g. a tjs language + autocomplete) */
|
|
42
|
+
languageExtension?: Extension;
|
|
43
|
+
onChange?: (value: string) => void;
|
|
44
|
+
/** style-injection root — pass the host's ShadowRoot when mounted in shadow DOM */
|
|
45
|
+
root?: Document | ShadowRoot;
|
|
46
|
+
}
|
|
47
|
+
/** Create a CodeMirror editor mounted in `parent`, returning an editor-agnostic handle. */
|
|
48
|
+
export declare function createCmEditor(parent: HTMLElement, opts?: CmOptions): CmHandle;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/*
|
|
2
|
+
CodeMirror 6 editor factory — the "heavy" module.
|
|
3
|
+
|
|
4
|
+
`code-editor.ts` imports this **dynamically** (on first `<tosi-code>` connect), so
|
|
5
|
+
CodeMirror only enters a consumer's bundle as a separate lazy chunk when the editor
|
|
6
|
+
is actually used — mirroring the way the old ACE build loaded from a CDN on first
|
|
7
|
+
use. No `<tosi-code>` in the page → no CodeMirror.
|
|
8
|
+
|
|
9
|
+
Languages here are the always-available set (js/ts/css/html/markdown). First-class
|
|
10
|
+
tjs (highlighting + runtime-value autocomplete via tjs-lang's editors/codemirror) is
|
|
11
|
+
layered in separately and only when tjs-lang is installed.
|
|
12
|
+
*/
|
|
13
|
+
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine } from '@codemirror/view';
|
|
14
|
+
import { EditorState, Compartment } from '@codemirror/state';
|
|
15
|
+
import { indentWithTab, history, defaultKeymap, historyKeymap, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
|
16
|
+
import { indentUnit, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching } from '@codemirror/language';
|
|
17
|
+
import { closeBrackets, closeBracketsKeymap, autocompletion, completionKeymap } from '@codemirror/autocomplete';
|
|
18
|
+
import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
|
|
19
|
+
import { lintKeymap } from '@codemirror/lint';
|
|
20
|
+
import { javascript } from '@codemirror/lang-javascript';
|
|
21
|
+
import { css } from '@codemirror/lang-css';
|
|
22
|
+
import { html } from '@codemirror/lang-html';
|
|
23
|
+
import { markdown } from '@codemirror/lang-markdown';
|
|
24
|
+
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
|
25
|
+
// The doc-system flips dark mode with a `darkmode` class on <body> (which inverts
|
|
26
|
+
// the luminance of --code-bg / --text-color, so the editor SURFACE adapts for
|
|
27
|
+
// free). Only CM's syntax-token colors need swapping: the light defaultHighlightStyle
|
|
28
|
+
// washes out on a dark surface, so use oneDark's brighter palette in dark mode.
|
|
29
|
+
const isDarkMode = () => typeof document !== 'undefined' &&
|
|
30
|
+
!!document.body &&
|
|
31
|
+
document.body.classList.contains('darkmode');
|
|
32
|
+
const highlightFor = (dark) => dark
|
|
33
|
+
? syntaxHighlighting(oneDarkHighlightStyle)
|
|
34
|
+
: syntaxHighlighting(defaultHighlightStyle, { fallback: true });
|
|
35
|
+
const isTjsMode = (mode) => mode === 'tjs' || mode === 'ajs';
|
|
36
|
+
/** Map a `<tosi-code mode>` value to a CodeMirror language extension. */
|
|
37
|
+
export function languageForMode(mode) {
|
|
38
|
+
switch (mode) {
|
|
39
|
+
case 'js':
|
|
40
|
+
case 'javascript':
|
|
41
|
+
return javascript();
|
|
42
|
+
case 'ts':
|
|
43
|
+
case 'typescript':
|
|
44
|
+
// tjs highlights as TypeScript here (the sync default). First-class tjs — the
|
|
45
|
+
// tjs-lang language + autocomplete — is layered on asynchronously via
|
|
46
|
+
// `loadTjsExtension()` + `setLanguageExtension()`, since it lazy-loads the
|
|
47
|
+
// (optional) tjs-lang editor build; this keeps `mode="tjs"` readable meanwhile.
|
|
48
|
+
case 'tjs':
|
|
49
|
+
case 'ajs':
|
|
50
|
+
return javascript({ typescript: true });
|
|
51
|
+
case 'css':
|
|
52
|
+
return css();
|
|
53
|
+
case 'html':
|
|
54
|
+
return html();
|
|
55
|
+
case 'md':
|
|
56
|
+
case 'markdown':
|
|
57
|
+
return markdown();
|
|
58
|
+
default:
|
|
59
|
+
return javascript();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// The language compartment carries the language AND its completion source, so
|
|
63
|
+
// exactly ONE `autocompletion()` is ever active: standard modes use CM's built-in
|
|
64
|
+
// (language-provided) completions here, while the tjs bundle brings its own
|
|
65
|
+
// `autocompletion({ override: [tjsCompletionSource] })`. (This is why the base
|
|
66
|
+
// extension set below does NOT include `autocompletion()`.)
|
|
67
|
+
function standardLanguageBundle(mode) {
|
|
68
|
+
return [languageForMode(mode), autocompletion()];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Lazy-load tjs-lang's CodeMirror language + completion bundle. Returns a single
|
|
72
|
+
* CodeMirror `Extension` (tjs language, forbidden-keyword highlighting, theme, and
|
|
73
|
+
* `tjsCompletionSource`-driven autocomplete) suitable for `setLanguageExtension()`,
|
|
74
|
+
* or `null` when tjs-lang (an optional peer) isn't installed or its editor build
|
|
75
|
+
* lacks `tjsEditorExtension`. The dynamic import keeps it out of the bundle until a
|
|
76
|
+
* tjs editor actually mounts.
|
|
77
|
+
*/
|
|
78
|
+
export async function loadTjsExtension(autocomplete = {}) {
|
|
79
|
+
try {
|
|
80
|
+
const mod = (await import('tjs-lang/editors/codemirror'));
|
|
81
|
+
if (typeof mod.tjsEditorExtension === 'function') {
|
|
82
|
+
return mod.tjsEditorExtension({ typescript: true, autocomplete });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// tjs-lang absent or editor build unavailable — caller keeps TS highlighting.
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
// A minimal theme so the editor has a real surface instead of showing whatever is
|
|
91
|
+
// behind it. It's applied as a CodeMirror theme (not shadow-DOM CSS) so it beats
|
|
92
|
+
// CM's injected base styles. Colors come from the doc-system theme vars
|
|
93
|
+
// (`--code-bg` / `--text-color`, which pierce the shadow root and adapt to dark
|
|
94
|
+
// mode) with sensible standalone fallbacks.
|
|
95
|
+
const editorTheme = EditorView.theme({
|
|
96
|
+
'&': {
|
|
97
|
+
height: '100%',
|
|
98
|
+
backgroundColor: 'var(--code-bg, var(--input-bg, #fdfdfd))',
|
|
99
|
+
color: 'var(--text-color, #222)',
|
|
100
|
+
},
|
|
101
|
+
'.cm-scroller': {
|
|
102
|
+
fontFamily: "Menlo, Monaco, Consolas, 'Courier New', monospace",
|
|
103
|
+
},
|
|
104
|
+
'.cm-gutters': {
|
|
105
|
+
backgroundColor: 'var(--code-bg, var(--input-bg, #fdfdfd))',
|
|
106
|
+
color: 'var(--text-color, #888)',
|
|
107
|
+
border: 'none',
|
|
108
|
+
},
|
|
109
|
+
'.cm-activeLine': { backgroundColor: 'transparent' },
|
|
110
|
+
'.cm-activeLineGutter': { backgroundColor: 'transparent' },
|
|
111
|
+
});
|
|
112
|
+
// A basicSetup-equivalent without a hard-coded color theme, so the host page's CSS
|
|
113
|
+
// (and dark mode) controls the palette. Line-wrapping mirrors the old ACE `wrap:true`.
|
|
114
|
+
function baseExtensions() {
|
|
115
|
+
return [
|
|
116
|
+
editorTheme,
|
|
117
|
+
lineNumbers(),
|
|
118
|
+
highlightActiveLineGutter(),
|
|
119
|
+
highlightSpecialChars(),
|
|
120
|
+
history(),
|
|
121
|
+
foldGutter(),
|
|
122
|
+
drawSelection(),
|
|
123
|
+
dropCursor(),
|
|
124
|
+
EditorState.allowMultipleSelections.of(true),
|
|
125
|
+
indentOnInput(),
|
|
126
|
+
indentUnit.of(' '),
|
|
127
|
+
EditorState.tabSize.of(2),
|
|
128
|
+
// syntax highlighting is applied per-instance via a compartment (dark-mode aware)
|
|
129
|
+
bracketMatching(),
|
|
130
|
+
closeBrackets(),
|
|
131
|
+
// NB: autocompletion() lives in the language compartment (see
|
|
132
|
+
// standardLanguageBundle) so tjs's own completion source never doubles up.
|
|
133
|
+
rectangularSelection(),
|
|
134
|
+
crosshairCursor(),
|
|
135
|
+
highlightActiveLine(),
|
|
136
|
+
highlightSelectionMatches(),
|
|
137
|
+
EditorView.lineWrapping,
|
|
138
|
+
keymap.of([
|
|
139
|
+
indentWithTab,
|
|
140
|
+
...closeBracketsKeymap,
|
|
141
|
+
...defaultKeymap,
|
|
142
|
+
...searchKeymap,
|
|
143
|
+
...historyKeymap,
|
|
144
|
+
...foldKeymap,
|
|
145
|
+
...completionKeymap,
|
|
146
|
+
...lintKeymap,
|
|
147
|
+
]),
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
/** Create a CodeMirror editor mounted in `parent`, returning an editor-agnostic handle. */
|
|
151
|
+
export function createCmEditor(parent, opts = {}) {
|
|
152
|
+
const language = new Compartment();
|
|
153
|
+
const readonly = new Compartment();
|
|
154
|
+
const highlight = new Compartment();
|
|
155
|
+
const langExt = opts.languageExtension ?? standardLanguageBundle(opts.mode ?? 'javascript');
|
|
156
|
+
const view = new EditorView({
|
|
157
|
+
parent,
|
|
158
|
+
root: opts.root,
|
|
159
|
+
state: EditorState.create({
|
|
160
|
+
doc: opts.value ?? '',
|
|
161
|
+
extensions: [
|
|
162
|
+
baseExtensions(),
|
|
163
|
+
highlight.of(highlightFor(isDarkMode())),
|
|
164
|
+
language.of(langExt),
|
|
165
|
+
readonly.of(EditorState.readOnly.of(!!opts.readOnly)),
|
|
166
|
+
EditorView.updateListener.of((u) => {
|
|
167
|
+
if (u.docChanged && opts.onChange)
|
|
168
|
+
opts.onChange(u.state.doc.toString());
|
|
169
|
+
}),
|
|
170
|
+
],
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
// Re-apply the right highlight palette when the doc-system toggles dark mode
|
|
174
|
+
// (it flips the `darkmode` class on <body>).
|
|
175
|
+
let dark = isDarkMode();
|
|
176
|
+
const darkObserver = typeof MutationObserver !== 'undefined' && typeof document !== 'undefined'
|
|
177
|
+
? new MutationObserver(() => {
|
|
178
|
+
const nowDark = isDarkMode();
|
|
179
|
+
if (nowDark !== dark) {
|
|
180
|
+
dark = nowDark;
|
|
181
|
+
view.dispatch({ effects: highlight.reconfigure(highlightFor(dark)) });
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
: null;
|
|
185
|
+
darkObserver?.observe(document.body, { attributes: true, attributeFilter: ['class'] });
|
|
186
|
+
const handle = {
|
|
187
|
+
view,
|
|
188
|
+
getValue: () => view.state.doc.toString(),
|
|
189
|
+
setValue(text) {
|
|
190
|
+
if (view.state.doc.toString() === text)
|
|
191
|
+
return;
|
|
192
|
+
view.dispatch({
|
|
193
|
+
changes: { from: 0, to: view.state.doc.length, insert: text },
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
setMode(mode) {
|
|
197
|
+
view.dispatch({ effects: language.reconfigure(standardLanguageBundle(mode)) });
|
|
198
|
+
},
|
|
199
|
+
setLanguageExtension(ext) {
|
|
200
|
+
view.dispatch({ effects: language.reconfigure(ext) });
|
|
201
|
+
},
|
|
202
|
+
setReadOnly(ro) {
|
|
203
|
+
view.dispatch({
|
|
204
|
+
effects: readonly.reconfigure(EditorState.readOnly.of(ro)),
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
undo: () => void undo(view),
|
|
208
|
+
redo: () => void redo(view),
|
|
209
|
+
canUndo: () => undoDepth(view.state) > 0,
|
|
210
|
+
canRedo: () => redoDepth(view.state) > 0,
|
|
211
|
+
focus: () => view.focus(),
|
|
212
|
+
refresh: () => view.requestMeasure(),
|
|
213
|
+
destroy: () => {
|
|
214
|
+
darkObserver?.disconnect();
|
|
215
|
+
view.destroy();
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
return handle;
|
|
219
|
+
}
|
package/dist/code-editor.d.ts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { Component as WebComponent, ElementCreator } from 'tosijs';
|
|
2
|
-
|
|
1
|
+
import { Component as WebComponent, ElementCreator, PartsMap } from 'tosijs';
|
|
2
|
+
import type { CmHandle } from './code-editor-cm';
|
|
3
|
+
interface CodeEditorParts extends PartsMap {
|
|
4
|
+
host: HTMLDivElement;
|
|
5
|
+
}
|
|
6
|
+
export declare class CodeEditor extends WebComponent<CodeEditorParts> {
|
|
3
7
|
static preferredTagName: string;
|
|
4
8
|
private source;
|
|
9
|
+
private _handle;
|
|
10
|
+
private _loadPromise;
|
|
11
|
+
private _appliedMode;
|
|
12
|
+
private _appliedDisabled;
|
|
5
13
|
get value(): string;
|
|
6
14
|
set value(text: string);
|
|
7
15
|
private _original;
|
|
@@ -12,16 +20,16 @@ export declare class CodeEditor extends WebComponent {
|
|
|
12
20
|
showDiff(on: boolean): void;
|
|
13
21
|
static initAttributes: {
|
|
14
22
|
mode: string;
|
|
15
|
-
theme: string;
|
|
16
23
|
disabled: boolean;
|
|
17
24
|
};
|
|
18
25
|
role: string;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
/** The underlying CodeMirror EditorView (undefined until loaded). */
|
|
27
|
+
get editor(): CmHandle['view'] | undefined;
|
|
28
|
+
undo(): void;
|
|
29
|
+
redo(): void;
|
|
30
|
+
canUndo(): boolean;
|
|
31
|
+
canRedo(): boolean;
|
|
32
|
+
content: () => HTMLDivElement[];
|
|
25
33
|
static shadowStyleSpec: {
|
|
26
34
|
':host': {
|
|
27
35
|
display: string;
|
|
@@ -29,9 +37,27 @@ export declare class CodeEditor extends WebComponent {
|
|
|
29
37
|
width: string;
|
|
30
38
|
height: string;
|
|
31
39
|
};
|
|
40
|
+
'[part="host"]': {
|
|
41
|
+
height: string;
|
|
42
|
+
};
|
|
43
|
+
'.cm-editor': {
|
|
44
|
+
height: string;
|
|
45
|
+
};
|
|
46
|
+
'.cm-scroller': {
|
|
47
|
+
outline: string;
|
|
48
|
+
fontFamily: string;
|
|
49
|
+
};
|
|
32
50
|
};
|
|
33
51
|
onResize(): void;
|
|
34
52
|
connectedCallback(): void;
|
|
53
|
+
private isTjsMode;
|
|
54
|
+
/**
|
|
55
|
+
* When in tjs mode, lazily upgrade the editor to tjs-lang's CodeMirror language +
|
|
56
|
+
* autocomplete. No-op (keeps TS highlighting) if not tjs, if tjs-lang isn't
|
|
57
|
+
* installed, or if the mode/handle changed before the async load resolved.
|
|
58
|
+
*/
|
|
59
|
+
private applyTjsExtension;
|
|
35
60
|
render(): void;
|
|
36
61
|
}
|
|
37
62
|
export declare const codeEditor: ElementCreator<CodeEditor>;
|
|
63
|
+
export {};
|
package/dist/code-editor.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/*#
|
|
2
2
|
# code
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
A [CodeMirror 6](https://codemirror.net/) wrapper.
|
|
5
5
|
|
|
6
6
|
Sometimes, it's nice to be able to just toss a code-editor in a web-page.
|
|
7
7
|
|
|
8
|
-
`<tosi-code>`'s `value` is the code it contains. Its `mode` attribute sets the
|
|
9
|
-
|
|
8
|
+
`<tosi-code>`'s `value` is the code it contains. Its `mode` attribute sets the
|
|
9
|
+
language (`javascript`, `typescript`, `tjs`, `css`, `html`, `markdown`).
|
|
10
10
|
|
|
11
11
|
```html
|
|
12
12
|
<tosi-code style="width: 100%; height: 100%" mode="css">
|
|
@@ -16,51 +16,33 @@ body {
|
|
|
16
16
|
</tosi-code>
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The `<tosi-code>` element has an `editor` property that gives you its
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
The `<tosi-code>` element has an `editor` property that gives you its CodeMirror
|
|
20
|
+
[`EditorView`](https://codemirror.net/docs/ref/#view.EditorView), and
|
|
21
|
+
`undo()` / `redo()` / `canUndo()` / `canRedo()` methods for history control.
|
|
22
|
+
|
|
23
|
+
CodeMirror is loaded lazily on first use — a page with no `<tosi-code>` bundles
|
|
24
|
+
none of it.
|
|
22
25
|
*/
|
|
23
26
|
/*{ "parent": "Components" }*/
|
|
24
|
-
import { Component as WebComponent } from 'tosijs';
|
|
25
|
-
import { scriptTag } from './via-tag';
|
|
27
|
+
import { Component as WebComponent, elements } from 'tosijs';
|
|
26
28
|
import { tosiDiff } from './diff';
|
|
27
|
-
const
|
|
28
|
-
const DEFAULT_THEME = 'ace/theme/tomorrow';
|
|
29
|
-
const getAce = async () => {
|
|
30
|
-
const { ace } = await scriptTag(`${ACE_BASE_URL}ace.min.js`);
|
|
31
|
-
return ace;
|
|
32
|
-
};
|
|
33
|
-
const makeCodeEditor = async (codeElement, mode = 'html', options = {}, theme = DEFAULT_THEME) => {
|
|
34
|
-
const ace = await getAce();
|
|
35
|
-
ace.config.set('basePath', ACE_BASE_URL);
|
|
36
|
-
const editor = ace.edit(codeElement, {
|
|
37
|
-
mode: `ace/mode/${mode}`,
|
|
38
|
-
tabSize: 2,
|
|
39
|
-
useSoftTabs: true,
|
|
40
|
-
useWorker: false,
|
|
41
|
-
// Wrap wide lines instead of scrolling horizontally; indentedSoftWrap keeps
|
|
42
|
-
// wrapped continuations aligned under the line's indent.
|
|
43
|
-
wrap: true,
|
|
44
|
-
indentedSoftWrap: true,
|
|
45
|
-
...options,
|
|
46
|
-
});
|
|
47
|
-
editor.setTheme(theme);
|
|
48
|
-
return { ace, editor };
|
|
49
|
-
};
|
|
29
|
+
const { div } = elements;
|
|
50
30
|
export class CodeEditor extends WebComponent {
|
|
51
31
|
static preferredTagName = 'tosi-code';
|
|
52
32
|
source = '';
|
|
33
|
+
_handle;
|
|
34
|
+
_loadPromise;
|
|
35
|
+
_appliedMode = '';
|
|
36
|
+
_appliedDisabled;
|
|
53
37
|
get value() {
|
|
54
|
-
return this.
|
|
38
|
+
return this._handle ? this._handle.getValue() : this.source;
|
|
55
39
|
}
|
|
56
40
|
set value(text) {
|
|
57
|
-
if (this.
|
|
58
|
-
this.
|
|
41
|
+
if (this._handle) {
|
|
42
|
+
this._handle.setValue(text);
|
|
59
43
|
}
|
|
60
44
|
else {
|
|
61
|
-
this.
|
|
62
|
-
this.editor.clearSelection();
|
|
63
|
-
this.editor.session.getUndoManager().reset();
|
|
45
|
+
this.source = text;
|
|
64
46
|
}
|
|
65
47
|
}
|
|
66
48
|
// Baseline for `showDiff()` — the version to diff the current `value` against.
|
|
@@ -72,9 +54,9 @@ export class CodeEditor extends WebComponent {
|
|
|
72
54
|
set original(text) {
|
|
73
55
|
this._original = text;
|
|
74
56
|
}
|
|
75
|
-
// Diff overlay —
|
|
76
|
-
//
|
|
77
|
-
// editor
|
|
57
|
+
// Diff overlay — built only on the editor's public surface (`value` + `original`)
|
|
58
|
+
// and the tosi-diff component, never the underlying editor's API, so it is
|
|
59
|
+
// editor-agnostic (this is why it survived the Ace → CodeMirror swap untouched).
|
|
78
60
|
diffOverlay;
|
|
79
61
|
get showingDiff() {
|
|
80
62
|
return this.diffOverlay !== undefined && !this.diffOverlay.hidden;
|
|
@@ -103,20 +85,27 @@ export class CodeEditor extends WebComponent {
|
|
|
103
85
|
}
|
|
104
86
|
static initAttributes = {
|
|
105
87
|
mode: 'javascript',
|
|
106
|
-
theme: DEFAULT_THEME,
|
|
107
88
|
disabled: false,
|
|
108
89
|
};
|
|
109
90
|
role = 'code editor';
|
|
110
|
-
|
|
111
|
-
_editor;
|
|
112
|
-
_editorPromise;
|
|
113
|
-
options = {};
|
|
114
|
-
get ace() {
|
|
115
|
-
return this._ace;
|
|
116
|
-
}
|
|
91
|
+
/** The underlying CodeMirror EditorView (undefined until loaded). */
|
|
117
92
|
get editor() {
|
|
118
|
-
return this.
|
|
93
|
+
return this._handle?.view;
|
|
94
|
+
}
|
|
95
|
+
// History control — so consumers use these instead of reaching into `editor`.
|
|
96
|
+
undo() {
|
|
97
|
+
this._handle?.undo();
|
|
98
|
+
}
|
|
99
|
+
redo() {
|
|
100
|
+
this._handle?.redo();
|
|
101
|
+
}
|
|
102
|
+
canUndo() {
|
|
103
|
+
return this._handle?.canUndo() ?? false;
|
|
104
|
+
}
|
|
105
|
+
canRedo() {
|
|
106
|
+
return this._handle?.canRedo() ?? false;
|
|
119
107
|
}
|
|
108
|
+
content = () => [div({ part: 'host' })];
|
|
120
109
|
static shadowStyleSpec = {
|
|
121
110
|
':host': {
|
|
122
111
|
display: 'block',
|
|
@@ -124,32 +113,71 @@ export class CodeEditor extends WebComponent {
|
|
|
124
113
|
width: '100%',
|
|
125
114
|
height: '100%',
|
|
126
115
|
},
|
|
116
|
+
'[part="host"]': { height: '100%' },
|
|
117
|
+
'.cm-editor': { height: '100%' },
|
|
118
|
+
'.cm-scroller': {
|
|
119
|
+
outline: 'none',
|
|
120
|
+
fontFamily: "Menlo, Monaco, Consolas, 'Courier New', monospace",
|
|
121
|
+
},
|
|
127
122
|
};
|
|
128
123
|
onResize() {
|
|
129
|
-
|
|
130
|
-
this.editor.resize(true);
|
|
131
|
-
}
|
|
124
|
+
this._handle?.refresh();
|
|
132
125
|
}
|
|
133
126
|
connectedCallback() {
|
|
134
127
|
super.connectedCallback();
|
|
135
128
|
if (this.source === '') {
|
|
136
129
|
this.value = this.textContent !== null ? this.textContent.trim() : '';
|
|
137
130
|
}
|
|
138
|
-
if (this.
|
|
139
|
-
|
|
140
|
-
this.
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
131
|
+
if (this._loadPromise === undefined) {
|
|
132
|
+
// Lazy chunk — CodeMirror only enters the bundle here, on first editor use.
|
|
133
|
+
this._loadPromise = import('./code-editor-cm').then(({ createCmEditor }) => {
|
|
134
|
+
const handle = createCmEditor(this.parts.host, {
|
|
135
|
+
value: this.source,
|
|
136
|
+
mode: this.mode,
|
|
137
|
+
readOnly: this.disabled,
|
|
138
|
+
root: this.shadowRoot ?? undefined,
|
|
139
|
+
onChange: (value) => this.dispatchEvent(new CustomEvent('change', { detail: { value } })),
|
|
140
|
+
});
|
|
141
|
+
this._handle = handle;
|
|
142
|
+
this._appliedMode = this.mode;
|
|
143
|
+
this._appliedDisabled = this.disabled;
|
|
144
|
+
this.applyTjsExtension();
|
|
145
|
+
return handle;
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
+
isTjsMode() {
|
|
150
|
+
return this.mode === 'tjs' || this.mode === 'ajs';
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* When in tjs mode, lazily upgrade the editor to tjs-lang's CodeMirror language +
|
|
154
|
+
* autocomplete. No-op (keeps TS highlighting) if not tjs, if tjs-lang isn't
|
|
155
|
+
* installed, or if the mode/handle changed before the async load resolved.
|
|
156
|
+
*/
|
|
157
|
+
applyTjsExtension() {
|
|
158
|
+
const handle = this._handle;
|
|
159
|
+
if (!handle || !this.isTjsMode())
|
|
160
|
+
return;
|
|
161
|
+
import('./code-editor-cm')
|
|
162
|
+
.then(({ loadTjsExtension }) => loadTjsExtension())
|
|
163
|
+
.then((ext) => {
|
|
164
|
+
if (ext && this._handle === handle && this.isTjsMode()) {
|
|
165
|
+
handle.setLanguageExtension(ext);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
149
169
|
render() {
|
|
150
170
|
super.render();
|
|
151
|
-
if (this.
|
|
152
|
-
this.
|
|
171
|
+
if (this._handle) {
|
|
172
|
+
if (this.disabled !== this._appliedDisabled) {
|
|
173
|
+
this._handle.setReadOnly(this.disabled);
|
|
174
|
+
this._appliedDisabled = this.disabled;
|
|
175
|
+
}
|
|
176
|
+
if (this.mode !== this._appliedMode) {
|
|
177
|
+
this._handle.setMode(this.mode);
|
|
178
|
+
this._appliedMode = this.mode;
|
|
179
|
+
this.applyTjsExtension();
|
|
180
|
+
}
|
|
153
181
|
}
|
|
154
182
|
}
|
|
155
183
|
}
|
package/dist/doc-browser.js
CHANGED
|
@@ -472,6 +472,11 @@ export function createDocBrowser(options) {
|
|
|
472
472
|
}
|
|
473
473
|
});
|
|
474
474
|
}
|
|
475
|
+
// This instance's own sidenav. Captured from its own change event (below), so the
|
|
476
|
+
// header nav toggle drives THIS doc-browser — not `document.querySelector('tosi-
|
|
477
|
+
// sidenav')`, which returns the FIRST sidenav in the document (an OUTER instance
|
|
478
|
+
// when this one is an embedded, memory-routed browser).
|
|
479
|
+
let sidenav = null;
|
|
475
480
|
const headerContent = [
|
|
476
481
|
button({
|
|
477
482
|
class: 'iconic',
|
|
@@ -489,8 +494,8 @@ export function createDocBrowser(options) {
|
|
|
489
494
|
},
|
|
490
495
|
},
|
|
491
496
|
onClick() {
|
|
492
|
-
|
|
493
|
-
|
|
497
|
+
if (sidenav)
|
|
498
|
+
sidenav.contentVisible = !sidenav.contentVisible;
|
|
494
499
|
},
|
|
495
500
|
}, icons.menu()),
|
|
496
501
|
span({ style: { flex: '0 0 10px' } }),
|
|
@@ -1027,9 +1032,12 @@ export function createDocBrowser(options) {
|
|
|
1027
1032
|
flex: '1 1 auto',
|
|
1028
1033
|
overflow: 'hidden',
|
|
1029
1034
|
},
|
|
1030
|
-
onChange() {
|
|
1031
|
-
|
|
1032
|
-
|
|
1035
|
+
onChange(event) {
|
|
1036
|
+
// Scope to the sidenav that fired — and capture it so the header toggle
|
|
1037
|
+
// targets THIS instance, not the first sidenav in the document.
|
|
1038
|
+
sidenav = event.target.closest(TosiSidenav.tagName);
|
|
1039
|
+
if (sidenav)
|
|
1040
|
+
app.compact = sidenav.compact;
|
|
1033
1041
|
},
|
|
1034
1042
|
}, searchField, navContent, div({
|
|
1035
1043
|
style: {
|
|
@@ -48,10 +48,10 @@ async function ensureHaltijaChannel(port) {
|
|
|
48
48
|
// HTTPS dev page has no mixed-content), while the `hj` CLI drives over its
|
|
49
49
|
// default HTTP 8700 — one server, both transports, shared state. HTTPS cert
|
|
50
50
|
// is mkcert-trusted. Output quiet so it doesn't drown the dev log.
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
spawn(['bunx', 'haltija@
|
|
51
|
+
// `@latest` (1.3.2+): the widget-mode WebRTC screen capture (which makes the
|
|
52
|
+
// Electron app CI-only) is on the stable channel now, along with the fix for
|
|
53
|
+
// an accidental Electron launch. (Was pinned to `@beta` before 1.3.2.)
|
|
54
|
+
spawn(['bunx', 'haltija@latest', '--server', '--both'], {
|
|
55
55
|
stdout: 'ignore',
|
|
56
56
|
stderr: 'ignore',
|
|
57
57
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { defineSiteConfig } from './site-config';
|
|
2
|
-
export type { SiteConfig, SiteHost } from './site-config';
|
|
2
|
+
export type { SiteConfig, SiteHost, LibraryBuildContext } from './site-config';
|
|
3
3
|
export { buildSite } from './orchestrator';
|
|
4
4
|
export { devServer } from './dev-server';
|
|
5
5
|
export { extractDocs, saveDocsJSON } from './docs';
|
|
@@ -102,6 +102,21 @@ export function generateLlmsTxt(outputPath, meta = {}, corpus) {
|
|
|
102
102
|
console.warn('llms.txt: no readable package.json — using config values only');
|
|
103
103
|
}
|
|
104
104
|
const entries = corpus ? entriesFromCorpus(corpus, meta) : entriesFromSrcScan();
|
|
105
|
+
// Only orient agents about the live-example execution model when the corpus
|
|
106
|
+
// actually has live examples (a book / pure-docs site has none).
|
|
107
|
+
const exampleFence = /```(js|tjs|ts|html|css)\b/;
|
|
108
|
+
const hasLiveExamples = !!corpus && corpus.some((d) => exampleFence.test(d.text ?? ''));
|
|
109
|
+
const liveExampleNote = hasLiveExamples
|
|
110
|
+
? [
|
|
111
|
+
'Live examples run inline on the shared page by default (not sandboxed):',
|
|
112
|
+
'each has its own `preview` element, but they share one tosijs module, so',
|
|
113
|
+
'`tosi()` state singletons are shared across every example on a page. This is',
|
|
114
|
+
'deliberate (it shows tosi isolation and lets you drive demos via the global',
|
|
115
|
+
'singleton). Namespace example state with unique top-level keys so examples',
|
|
116
|
+
"don't clobber each other; the `iframe` attribute isolates DOM/CSS, not state.",
|
|
117
|
+
'',
|
|
118
|
+
]
|
|
119
|
+
: [];
|
|
105
120
|
const name = meta.name ?? pkg.name ?? '';
|
|
106
121
|
const description = meta.description ?? pkg.description ?? '';
|
|
107
122
|
const links = [];
|
|
@@ -125,6 +140,7 @@ export function generateLlmsTxt(outputPath, meta = {}, corpus) {
|
|
|
125
140
|
'',
|
|
126
141
|
'Full documentation, with live code examples, is at the links below.',
|
|
127
142
|
'',
|
|
143
|
+
...liveExampleNote,
|
|
128
144
|
'## Pages',
|
|
129
145
|
'',
|
|
130
146
|
];
|