@stainless-api/playgrounds 0.0.1-beta.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +23 -0
  3. package/eslint.config.js +2 -0
  4. package/package.json +69 -0
  5. package/src/Logs.tsx +216 -0
  6. package/src/Panel.tsx +21 -0
  7. package/src/PlaygroundPanelWrapper.tsx +5 -0
  8. package/src/build-py-types.ts +152 -0
  9. package/src/build-ts-types.ts +70 -0
  10. package/src/build.ts +97 -0
  11. package/src/codemirror/comlink.ts +698 -0
  12. package/src/codemirror/curl/curlconverter.vendor.js +7959 -0
  13. package/src/codemirror/curl.ts +108 -0
  14. package/src/codemirror/deps.ts +12 -0
  15. package/src/codemirror/fix-lsp-markdown.ts +50 -0
  16. package/src/codemirror/lsp.ts +87 -0
  17. package/src/codemirror/python/anser.ts +398 -0
  18. package/src/codemirror/python/pyodide.ts +180 -0
  19. package/src/codemirror/python.ts +160 -0
  20. package/src/codemirror/react.tsx +615 -0
  21. package/src/codemirror/sanitize-html.ts +12 -0
  22. package/src/codemirror/shiki.ts +65 -0
  23. package/src/codemirror/typescript/cdn-typescript.d.ts +1 -0
  24. package/src/codemirror/typescript/cdn-typescript.js +1 -0
  25. package/src/codemirror/typescript/console.ts +590 -0
  26. package/src/codemirror/typescript/get-signature.ts +94 -0
  27. package/src/codemirror/typescript/prettier-plugin-external-typescript.vendor.js +4968 -0
  28. package/src/codemirror/typescript/runner.ts +396 -0
  29. package/src/codemirror/typescript/special-info.ts +171 -0
  30. package/src/codemirror/typescript/worker.ts +292 -0
  31. package/src/codemirror/typescript.tsx +198 -0
  32. package/src/create.tsx +44 -0
  33. package/src/icon.tsx +21 -0
  34. package/src/index.ts +6 -0
  35. package/src/logs-context.ts +5 -0
  36. package/src/playground.css +359 -0
  37. package/src/sandbox-worker/in-frame.js +179 -0
  38. package/src/sandbox-worker/index.ts +202 -0
  39. package/src/use-storage.ts +54 -0
  40. package/src/util.ts +29 -0
  41. package/src/virtual-module.d.ts +45 -0
  42. package/src/vite-env.d.ts +1 -0
  43. package/test/get-signature.test.ts +73 -0
  44. package/test/use-storage.test.ts +60 -0
  45. package/tsconfig.json +11 -0
@@ -0,0 +1,292 @@
1
+ import { createVirtualTypeScriptEnvironment, VirtualTypeScriptEnvironment } from '@typescript/vfs';
2
+ import ts from './cdn-typescript';
3
+ import * as Comlink from '../comlink';
4
+ import { createWorker, HoverInfo } from '@stainless-api/codemirror-ts/worker';
5
+ import { format } from 'prettier';
6
+ import { parsers } from './prettier-plugin-external-typescript.vendor';
7
+ import estree from 'prettier/plugins/estree';
8
+ import { SourceMapConsumer } from 'source-map';
9
+ import { LinesAndColumns } from 'lines-and-columns';
10
+
11
+ type TypescriptTypes = NonNullable<typeof import('virtual:stl-playground/typescript.json').default>;
12
+
13
+ // @ts-expect-error this is kinda awful but it makes ts autocomplete work without source patching
14
+ Object.prototype.keepLegacyLimitationForAutocompletionSymbols = false;
15
+
16
+ function escapeRegExp(text: string) {
17
+ return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
18
+ }
19
+ let realpathRe;
20
+ const realpath = (typescriptTypes: TypescriptTypes, path: string) => {
21
+ realpathRe ??= new RegExp(
22
+ `^(${typescriptTypes!.links.map((e, i) => `(?<_${i}>${escapeRegExp(e[0])})`).join('|')})(/|$)`,
23
+ );
24
+ let match;
25
+ while ((match = path.match(realpathRe))) {
26
+ const entry =
27
+ typescriptTypes!.links[
28
+ Object.entries(match.groups!)
29
+ .find((e) => e[1])![0]
30
+ .slice(1) as `${number}`
31
+ ]!;
32
+ path = entry[1] + (match[2 + typescriptTypes!.links.length] ?? '') + path.slice(match[0].length);
33
+ }
34
+ return path;
35
+ };
36
+
37
+ function notImplemented(methodName: string): never {
38
+ throw new Error(`Method '${methodName}' is not implemented.`);
39
+ }
40
+
41
+ function audit<ArgsT extends unknown[], ReturnT>(
42
+ _name: string,
43
+ fn: (...args: ArgsT) => ReturnT,
44
+ ): (...args: ArgsT) => ReturnT {
45
+ return (...args) => {
46
+ const res = fn(...args);
47
+
48
+ //const smallRes = typeof res === "string" ? res.slice(0, 80) + "..." : res;
49
+ //debugLog("> " + name, ...args);
50
+ //debugLog("< " + smallRes);
51
+
52
+ return res;
53
+ };
54
+ }
55
+
56
+ // "/DOM.d.ts" => "/lib.dom.d.ts"
57
+ const libize = (path: string) => path.replace('/', '/lib.').toLowerCase();
58
+
59
+ /**
60
+ * Creates an in-memory System object which can be used in a TypeScript program, this
61
+ * is what provides read/write aspects of the virtual fs
62
+ */
63
+ export function createSystem(typescriptTypes: TypescriptTypes, files: Map<string, string>): ts.System {
64
+ return {
65
+ args: [],
66
+ createDirectory: () => notImplemented('createDirectory'),
67
+ // TODO: could make a real file tree
68
+ directoryExists: audit('directoryExists', (directory) => {
69
+ directory = realpath(typescriptTypes, directory);
70
+ return Array.from(files.keys()).some((path) => path.startsWith(directory));
71
+ }),
72
+ exit: () => notImplemented('exit'),
73
+ fileExists: audit(
74
+ 'fileExists',
75
+ (fileName) =>
76
+ files.has(realpath(typescriptTypes, fileName)) ||
77
+ files.has(realpath(typescriptTypes, libize(fileName))),
78
+ ),
79
+ getCurrentDirectory: () => '/',
80
+ getDirectories: () => [],
81
+ getExecutingFilePath: () => notImplemented('getExecutingFilePath'),
82
+ readDirectory: audit('readDirectory', (directory) => (directory === '/' ? Array.from(files.keys()) : [])),
83
+ readFile: audit(
84
+ 'readFile',
85
+ (fileName) =>
86
+ files.get(realpath(typescriptTypes, fileName)) ??
87
+ files.get(realpath(typescriptTypes, libize(fileName))),
88
+ ),
89
+ resolvePath: (path) => realpath(typescriptTypes, path),
90
+ newLine: '\n',
91
+ useCaseSensitiveFileNames: true,
92
+ write: () => notImplemented('write'),
93
+ writeFile: (fileName, contents) => {
94
+ files.set(realpath(typescriptTypes, fileName), contents);
95
+ },
96
+ deleteFile: (fileName) => {
97
+ files.delete(realpath(typescriptTypes, fileName));
98
+ },
99
+ };
100
+ }
101
+
102
+ export type PromiseWithResolvers<T> = {
103
+ promise: Promise<T>;
104
+ resolve: (value: T) => void;
105
+ reject: (reason?: unknown) => void;
106
+ };
107
+ function promiseWithResolvers<T>(): PromiseWithResolvers<T> {
108
+ let resolve, reject;
109
+ const promise = new Promise<T>((_resolve, _reject) => {
110
+ resolve = _resolve;
111
+ reject = _reject;
112
+ });
113
+ return {
114
+ promise,
115
+ resolve: resolve!,
116
+ reject: reject!,
117
+ };
118
+ }
119
+
120
+ const compilerOpts: ts.CompilerOptions = {
121
+ module: ts.ModuleKind.NodeNext,
122
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
123
+ lib: ['esnext', 'webworker', 'webworker.iterable'],
124
+ types: ['stl-play'],
125
+ target: ts.ScriptTarget.ESNext,
126
+ moduleDetection: ts.ModuleDetectionKind.Force,
127
+ };
128
+
129
+ const initData = promiseWithResolvers<{
130
+ suggestedEnv: string[];
131
+ typescriptTypes: TypescriptTypes;
132
+ }>();
133
+
134
+ let typescriptTypes: TypescriptTypes;
135
+ let fsMap: Map<string, string>;
136
+ type RawCompletionItem = {
137
+ label: string;
138
+ type: Completion['type'];
139
+ } & Pick<ts.CompletionEntryDetails, 'codeActions' | 'displayParts' | 'documentation' | 'tags'>;
140
+ type RawCompletion = {
141
+ from: number;
142
+ options: RawCompletionItem[];
143
+ };
144
+ const worker: {
145
+ initialize(): Promise<void>;
146
+ updateFile({ path, code }: { path: string; code: string }): void;
147
+ getLints({ path }: { path: string }): Diagnostic[];
148
+ getAutocompletion({
149
+ path,
150
+ context,
151
+ }: {
152
+ path: string;
153
+ context: Pick<CompletionContext, 'pos' | 'explicit'>;
154
+ }): Promise<RawCompletion | null> | null;
155
+ getHover({ path, pos }: { path: string; pos: number }): HoverInfo | null;
156
+ getEnv(): VirtualTypeScriptEnvironment;
157
+ } = createWorker({
158
+ env: (async () => {
159
+ const { typescriptTypes, suggestedEnv } = await initData.promise;
160
+ fsMap = new Map<string, string>(typescriptTypes.files as [string, string][]);
161
+ fsMap.set(
162
+ '/node_modules/@types/stl-play/package.json',
163
+ JSON.stringify({
164
+ name: '@types/stl-play',
165
+ types: 'index.d.ts',
166
+ }),
167
+ );
168
+ fsMap.set(
169
+ '/node_modules/@types/stl-play/index.d.ts',
170
+ `declare global {
171
+ declare var process: NodeJS.Process;
172
+ declare namespace NodeJS {
173
+ interface Dict<T> {
174
+ [key: string]: T | undefined;
175
+ }
176
+ interface ProcessEnv extends Dict<string> {\n${['TZ', ...suggestedEnv].map((e) => ` ${JSON.stringify(e)}?: string | undefined;\n`)} }
177
+ interface Process {
178
+ /**
179
+ * The \`process.env\` property returns an object containing environment variables.
180
+ */
181
+ env: ProcessEnv;
182
+ }
183
+ }
184
+ }
185
+ export {}`,
186
+ );
187
+ const system = createSystem(typescriptTypes, fsMap);
188
+ return createVirtualTypeScriptEnvironment(system, [], ts, compilerOpts);
189
+ })(),
190
+ });
191
+ const api = {
192
+ ...worker,
193
+ async getHover(opts: { path: string; pos: number }): Promise<HoverInfo | null> {
194
+ const result = worker.getHover(opts);
195
+ if (result?.def) {
196
+ result.def = await Promise.all(result.def.map(fixDefInfo));
197
+ }
198
+ if (result?.typeDef) {
199
+ result.typeDef = await Promise.all(result.typeDef.map(fixDefInfo));
200
+ }
201
+ return result;
202
+ },
203
+ stlInit(initData_: Awaited<typeof initData.promise>) {
204
+ typescriptTypes = initData_.typescriptTypes;
205
+ initData.resolve(initData_);
206
+ },
207
+ async format(code: string) {
208
+ const result = (
209
+ await format(code, {
210
+ parser: 'typescript',
211
+ plugins: [{ parsers }, estree],
212
+ printWidth: 75,
213
+ singleQuote: true,
214
+ })
215
+ ).replace(/\n+$/, '');
216
+ if (result === code) return undefined;
217
+ return result;
218
+ },
219
+ async transform(code: string) {
220
+ return ts.transpileModule(code, {
221
+ fileName: 'input.mts',
222
+ compilerOptions: {
223
+ ...compilerOpts,
224
+ noCheck: true,
225
+ },
226
+ transformers: {
227
+ after: [
228
+ (ctx) => (file) =>
229
+ ts.visitEachChild(
230
+ file,
231
+ (child) =>
232
+ ts.isImportDeclaration(child)
233
+ ? ctx.factory.updateImportDeclaration(
234
+ child,
235
+ child.modifiers,
236
+ child.importClause,
237
+ ts.isStringLiteral(child.moduleSpecifier)
238
+ ? ctx.factory.createStringLiteral(
239
+ child.moduleSpecifier.text.replace(
240
+ /^(?!\.{0,2}\/|[a-z][a-z0-9+.-]+:)/,
241
+ 'https://esm.sh/',
242
+ ),
243
+ )
244
+ : child.moduleSpecifier,
245
+ child.attributes,
246
+ )
247
+ : child,
248
+ ctx,
249
+ ),
250
+ ],
251
+ },
252
+ }).outputText;
253
+ },
254
+ };
255
+
256
+ import mappingsWasm from 'source-map/lib/mappings.wasm?url&inline';
257
+ import { Diagnostic } from '@codemirror/lint';
258
+ import { Completion, CompletionContext } from '@codemirror/autocomplete';
259
+ // @ts-expect-error - this is untyped
260
+ SourceMapConsumer.initialize({ 'lib/mappings.wasm': mappingsWasm });
261
+
262
+ const mapCache = new Map<string, { tracer: SourceMapConsumer; loc: LinesAndColumns } | null>();
263
+ const fixDefInfo = async (def: ts.DefinitionInfo) => {
264
+ def.originalFileName = def.fileName.replace(/\.d\.m?ts$/, '.ts');
265
+ const path = realpath(typescriptTypes!, def.fileName + '.map');
266
+ let cached = mapCache.get(path);
267
+ if (cached === undefined) {
268
+ const dtsString = fsMap.get(realpath(typescriptTypes!, def.fileName));
269
+ const mapString = fsMap.get(path);
270
+ if (mapString && dtsString) {
271
+ cached = {
272
+ tracer: await new SourceMapConsumer(mapString),
273
+ loc: new LinesAndColumns(dtsString),
274
+ };
275
+ mapCache.set(path, cached);
276
+ } else {
277
+ mapCache.set(path, null);
278
+ }
279
+ }
280
+ if (cached) {
281
+ const loc = cached.loc.locationForIndex(def.textSpan.start);
282
+ if (loc) {
283
+ const { line } = cached.tracer.originalPositionFor(loc);
284
+ if (line) {
285
+ def.originalFileName += '#L' + line;
286
+ }
287
+ }
288
+ }
289
+ return def;
290
+ };
291
+ export type API = typeof api;
292
+ Comlink.expose(api);
@@ -0,0 +1,198 @@
1
+ import { typescriptLanguage } from '@codemirror/lang-javascript';
2
+ import tsWorkerURL from './typescript/worker?worker&url';
3
+ import runnerWorkerURL from './typescript/runner?worker&url';
4
+ import type { HoverInfo } from '@stainless-api/codemirror-ts/worker';
5
+ import { tsAutocomplete, tsFacet, tsHover, tsLinter, tsSync, tsTwoslash } from '@stainless-api/codemirror-ts';
6
+ import * as Comlink from './comlink';
7
+ import { autocompletion } from '@codemirror/autocomplete';
8
+ import type { JSDocTagInfo, QuickInfo, SymbolDisplayPart } from 'typescript';
9
+ import type { Language } from './react';
10
+ import { proxy, type Remote } from './comlink';
11
+ import type { Logger } from '../Logs';
12
+ import type { RunnerAPI } from './typescript/runner';
13
+ import typescriptTypes from 'virtual:stl-playground/typescript.json';
14
+ import authData from 'virtual:stl-playground/auth.json';
15
+ import { docToHTML } from './fix-lsp-markdown';
16
+ import { API } from './typescript/worker';
17
+ import { EditorView } from '@codemirror/view';
18
+
19
+ if (!typescriptTypes) {
20
+ throw new Error('TypeScript playgrounds failed to build.');
21
+ }
22
+
23
+ function displayPartsToString(displayParts: SymbolDisplayPart[] | undefined): string {
24
+ if (displayParts) {
25
+ return displayParts.map((displayPart) => displayPart.text).join('');
26
+ }
27
+ return '';
28
+ }
29
+
30
+ function maybeBlock(md: string) {
31
+ return markdown(md).startsWith('<p>') ? md : '\n' + md;
32
+ }
33
+
34
+ function tagToString(tag: JSDocTagInfo): string {
35
+ let tagLabel = `*@${tag.name}*`;
36
+ if (tag.name === 'param' && tag.text) {
37
+ const [paramName, ...rest] = tag.text;
38
+ tagLabel += ` \`${paramName!.text}\``;
39
+ if (rest.length > 0) tagLabel += ` — ${maybeBlock(rest.map((r) => r.text).join(' '))}`;
40
+ } else if (Array.isArray(tag.text)) {
41
+ tagLabel += ` — ${maybeBlock(tag.text.map((r) => r.text).join(' '))}`;
42
+ } else if (tag.text) {
43
+ tagLabel += ` — ${maybeBlock(tag.text)}`;
44
+ }
45
+ return tagLabel;
46
+ }
47
+
48
+ const markdown = (md: string) => {
49
+ return docToHTML(md, 'markdown');
50
+ };
51
+
52
+ const useMeta = /iPad|iPhone|iPod|Mac/.test(navigator.userAgent);
53
+ type GoToOptions = {
54
+ gotoHandler?: (currentPath: string, hoverData: HoverInfo, view: EditorView) => true | undefined;
55
+ };
56
+ export function tsGoto(opts: GoToOptions = {}) {
57
+ return EditorView.domEventHandlers({
58
+ mousedown: (event, view) => {
59
+ const config = view.state.facet(tsFacet);
60
+ if (!config?.worker || !opts.gotoHandler) return false;
61
+
62
+ if (!(useMeta ? event.metaKey : event.ctrlKey)) return false;
63
+
64
+ event.preventDefault();
65
+
66
+ const pos = view.posAtCoords({
67
+ x: event.clientX,
68
+ y: event.clientY,
69
+ });
70
+
71
+ if (pos === null) return;
72
+
73
+ config.worker
74
+ .getHover({
75
+ path: config.path,
76
+ pos,
77
+ })
78
+ .then((hoverData: HoverInfo | null) => {
79
+ if (hoverData && opts.gotoHandler) {
80
+ opts.gotoHandler(config.path, hoverData, view);
81
+ }
82
+ });
83
+
84
+ return true;
85
+ },
86
+ });
87
+ }
88
+
89
+ export async function createTypescript(signal: AbortSignal, doc: string): Promise<Language> {
90
+ const { SandboxWorker } = await import('../sandbox-worker/index.js');
91
+ const innerWorker: Worker = new SandboxWorker(tsWorkerURL, { name: 'typescript' });
92
+ signal.addEventListener('abort', () => innerWorker.terminate());
93
+ const worker = Comlink.wrap<API>(innerWorker);
94
+ await worker.stlInit({
95
+ typescriptTypes: typescriptTypes!,
96
+ suggestedEnv: authData!
97
+ .flatMap((e) => e.opts)
98
+ .map((e) => e.read_env)
99
+ .filter((_) => typeof _ === 'string'),
100
+ });
101
+ await worker.initialize();
102
+ function renderTooltip({ quickInfo }: HoverInfo) {
103
+ const info = quickInfo as QuickInfo;
104
+ const elt = document.createElement('div');
105
+ elt.className = 'cm-lsp-hover-tooltip cm-lsp-documentation';
106
+ const documentation = displayPartsToString(info.documentation);
107
+ const tags = info.tags ? info.tags.map((tag) => tagToString(tag)).join(' \n\n') : '';
108
+
109
+ return {
110
+ dom: Object.assign(document.createElement('div'), {
111
+ innerHTML: markdown(
112
+ displayPartsToString(info.displayParts)
113
+ .split('\n')
114
+ .map((e) => '```typescript\n' + e + '\n```\n')
115
+ .join('') +
116
+ (documentation ? '\n\n---\n' + documentation : '') +
117
+ (tags ? '\n\n---\n' + tags : ''),
118
+ ),
119
+ }),
120
+ };
121
+ }
122
+ let runnerWorker: Worker;
123
+ let runnerAPI: Remote<RunnerAPI>;
124
+ const startRunner = () => {
125
+ if (runnerWorker) {
126
+ runnerWorker.terminate();
127
+ runnerAPI[Comlink.releaseProxy]();
128
+ }
129
+ runnerWorker = new SandboxWorker(runnerWorkerURL);
130
+ runnerAPI = Comlink.wrap<RunnerAPI>(runnerWorker);
131
+ runnerAPI.setLogger(
132
+ proxy((...args) => {
133
+ logger(...args);
134
+ }),
135
+ );
136
+ };
137
+ startRunner();
138
+
139
+ let logger: Logger;
140
+ return {
141
+ transport: undefined,
142
+ fileName: 'playground.mts',
143
+ extensions: [
144
+ typescriptLanguage,
145
+ tsFacet.of({ worker, path: '/play/playground.mts' }),
146
+ tsSync(),
147
+ tsLinter(),
148
+ autocompletion({
149
+ override: [tsAutocomplete()],
150
+ }),
151
+ tsHover({
152
+ renderTooltip,
153
+ }),
154
+ tsGoto({
155
+ gotoHandler(_path, hoverData, view) {
156
+ const def = hoverData.def?.[0] ?? hoverData.typeDef?.[0];
157
+ if (def?.originalFileName === '/play/playground.mts') {
158
+ view.dispatch({ selection: { anchor: def.textSpan.start, head: def.textSpan.start } });
159
+ }
160
+ console.log(def?.originalFileName);
161
+ return undefined;
162
+ },
163
+ }),
164
+ tsTwoslash(),
165
+ ],
166
+ doc,
167
+ setLogger(fn) {
168
+ logger = fn;
169
+ },
170
+ async expandLogHandle(handle) {
171
+ return await runnerAPI.expandLogHandle(handle);
172
+ },
173
+ async freeLogHandle(handle) {
174
+ return await runnerAPI.freeLogHandle(handle);
175
+ },
176
+ async setEnv(vars) {
177
+ return await runnerAPI.setEnv(vars);
178
+ },
179
+ async run(code, signal) {
180
+ signal.addEventListener('abort', startRunner);
181
+ try {
182
+ const transpiled = await worker.transform(code);
183
+ await runnerAPI.run(transpiled);
184
+ } catch (e) {
185
+ if (signal.aborted) {
186
+ logger({ type: 'error', parts: ['Aborted.'] });
187
+ return;
188
+ }
189
+ throw e;
190
+ } finally {
191
+ signal.removeEventListener('abort', startRunner);
192
+ }
193
+ },
194
+ async format(code) {
195
+ return worker.format(code);
196
+ },
197
+ };
198
+ }
package/src/create.tsx ADDED
@@ -0,0 +1,44 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { signal } from '@preact/signals-core';
4
+ import { Editor } from './codemirror/react';
5
+ import { LogsContext } from './logs-context';
6
+ import './playground.css';
7
+
8
+ export type PlaygroundLanguage = 'python' | 'typescript' | 'http';
9
+
10
+ export function createPlayground(props: {
11
+ lang: PlaygroundLanguage;
12
+ doc: string;
13
+ /** div.stl-snippet-request-container */
14
+ container: HTMLElement;
15
+ }): () => Promise<void> {
16
+ const ready = new Promise<{ element: HTMLDivElement; onShow(): void }>((resolve) => {
17
+ const element = document.createElement('div');
18
+ const root = createRoot(element);
19
+ const nodes = (
20
+ <StrictMode>
21
+ <LogsContext value={signal([])}>
22
+ <Editor
23
+ {...props}
24
+ unmount={() => {
25
+ props.container.style.display = '';
26
+ root.unmount();
27
+ element.remove();
28
+ }}
29
+ onLoad={(onShow) => {
30
+ resolve({ element, onShow });
31
+ }}
32
+ />
33
+ </LogsContext>
34
+ </StrictMode>
35
+ );
36
+ root.render(nodes);
37
+ });
38
+ return () =>
39
+ ready.then(({ element, onShow }) => {
40
+ props.container.insertAdjacentElement('afterend', element);
41
+ props.container.style.display = 'none';
42
+ onShow();
43
+ });
44
+ }
package/src/icon.tsx ADDED
@@ -0,0 +1,21 @@
1
+ import style from '@stainless-api/docs-ui/style';
2
+
3
+ export function PlaygroundIcon() {
4
+ return (
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width={16}
8
+ height={16}
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth={2}
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ className={'lucide ' + style.Icon}
16
+ aria-hidden="true"
17
+ >
18
+ <path d="m 1,2 h 1 a 4,4 0 0 1 4,4 v 1 m 5,15 H 10 A 4,4 0 0 1 6,18 V 6 a 4,4 0 0 1 4,-4 h 1 M 1,22 H 2 A 4,4 0 0 0 6,18 V 17 M 14.029059,8.147837 A 1.2853426,1.2853426 0 0 1 15.978924,7.0437277 L 22.40178,10.8959 a 1.2853426,1.2853426 0 0 1 0,2.208219 l -6.422856,3.852172 a 1.2853426,1.2853426 0 0 1 -1.949865,-1.105395 z" />
19
+ </svg>
20
+ );
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ export default {
5
+ playgroundsBase: dirname(dirname(fileURLToPath(import.meta.url))),
6
+ };
@@ -0,0 +1,5 @@
1
+ import type { Signal } from '@preact/signals-core';
2
+ import type { StateLog } from './Logs';
3
+ import { createContext } from 'react';
4
+
5
+ export const LogsContext = createContext<Signal<StateLog[]> | undefined>(undefined);