@dxos/plugin-script 0.6.8-main.046e6cf

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 (136) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +4 -0
  3. package/dist/lib/browser/ScriptEditor-6Q7HFLJ6.mjs +220 -0
  4. package/dist/lib/browser/ScriptEditor-6Q7HFLJ6.mjs.map +7 -0
  5. package/dist/lib/browser/TypescriptEditor-HP35MTEQ.mjs +9 -0
  6. package/dist/lib/browser/TypescriptEditor-HP35MTEQ.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-2EM5Y66A.mjs +60 -0
  8. package/dist/lib/browser/chunk-2EM5Y66A.mjs.map +7 -0
  9. package/dist/lib/browser/chunk-EZ7PWOLU.mjs +235 -0
  10. package/dist/lib/browser/chunk-EZ7PWOLU.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-JBACGDOJ.mjs +21 -0
  12. package/dist/lib/browser/chunk-JBACGDOJ.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-Z72YKDJ2.mjs +31 -0
  14. package/dist/lib/browser/chunk-Z72YKDJ2.mjs.map +7 -0
  15. package/dist/lib/browser/edge/index.mjs +19 -0
  16. package/dist/lib/browser/edge/index.mjs.map +7 -0
  17. package/dist/lib/browser/index.mjs +395 -0
  18. package/dist/lib/browser/index.mjs.map +7 -0
  19. package/dist/lib/browser/meta.json +1 -0
  20. package/dist/lib/browser/meta.mjs +9 -0
  21. package/dist/lib/browser/meta.mjs.map +7 -0
  22. package/dist/lib/browser/types/index.mjs +10 -0
  23. package/dist/lib/browser/types/index.mjs.map +7 -0
  24. package/dist/lib/node/ScriptEditor-AIVKL2NX.cjs +239 -0
  25. package/dist/lib/node/ScriptEditor-AIVKL2NX.cjs.map +7 -0
  26. package/dist/lib/node/TypescriptEditor-PWOAN5RQ.cjs +30 -0
  27. package/dist/lib/node/TypescriptEditor-PWOAN5RQ.cjs.map +7 -0
  28. package/dist/lib/node/chunk-4VMNBXWU.cjs +54 -0
  29. package/dist/lib/node/chunk-4VMNBXWU.cjs.map +7 -0
  30. package/dist/lib/node/chunk-6Y2S6SGK.cjs +49 -0
  31. package/dist/lib/node/chunk-6Y2S6SGK.cjs.map +7 -0
  32. package/dist/lib/node/chunk-LWUBFTPA.cjs +253 -0
  33. package/dist/lib/node/chunk-LWUBFTPA.cjs.map +7 -0
  34. package/dist/lib/node/chunk-SLV3SDZE.cjs +93 -0
  35. package/dist/lib/node/chunk-SLV3SDZE.cjs.map +7 -0
  36. package/dist/lib/node/edge/index.cjs +41 -0
  37. package/dist/lib/node/edge/index.cjs.map +7 -0
  38. package/dist/lib/node/index.cjs +417 -0
  39. package/dist/lib/node/index.cjs.map +7 -0
  40. package/dist/lib/node/meta.cjs +30 -0
  41. package/dist/lib/node/meta.cjs.map +7 -0
  42. package/dist/lib/node/meta.json +1 -0
  43. package/dist/lib/node/types/index.cjs +32 -0
  44. package/dist/lib/node/types/index.cjs.map +7 -0
  45. package/dist/types/src/ScriptPlugin.d.ts +7 -0
  46. package/dist/types/src/ScriptPlugin.d.ts.map +1 -0
  47. package/dist/types/src/compiler/compiler.d.ts +38 -0
  48. package/dist/types/src/compiler/compiler.d.ts.map +1 -0
  49. package/dist/types/src/compiler/compiler.test.d.ts +2 -0
  50. package/dist/types/src/compiler/compiler.test.d.ts.map +1 -0
  51. package/dist/types/src/compiler/index.d.ts +2 -0
  52. package/dist/types/src/compiler/index.d.ts.map +1 -0
  53. package/dist/types/src/components/ScriptBlock/FrameContainer/FrameContainer.d.ts +12 -0
  54. package/dist/types/src/components/ScriptBlock/FrameContainer/FrameContainer.d.ts.map +1 -0
  55. package/dist/types/src/components/ScriptBlock/FrameContainer/frame.d.ts +2 -0
  56. package/dist/types/src/components/ScriptBlock/FrameContainer/frame.d.ts.map +1 -0
  57. package/dist/types/src/components/ScriptBlock/FrameContainer/index.d.ts +2 -0
  58. package/dist/types/src/components/ScriptBlock/FrameContainer/index.d.ts.map +1 -0
  59. package/dist/types/src/components/ScriptBlock/ScriptBlock.d.ts +18 -0
  60. package/dist/types/src/components/ScriptBlock/ScriptBlock.d.ts.map +1 -0
  61. package/dist/types/src/components/ScriptBlock/ScriptBlock.stories.d.ts +14 -0
  62. package/dist/types/src/components/ScriptBlock/ScriptBlock.stories.d.ts.map +1 -0
  63. package/dist/types/src/components/ScriptBlock/Splitter/Splitter.d.ts +15 -0
  64. package/dist/types/src/components/ScriptBlock/Splitter/Splitter.d.ts.map +1 -0
  65. package/dist/types/src/components/ScriptBlock/Splitter/Splitter.stories.d.ts +13 -0
  66. package/dist/types/src/components/ScriptBlock/Splitter/Splitter.stories.d.ts.map +1 -0
  67. package/dist/types/src/components/ScriptBlock/Splitter/index.d.ts +2 -0
  68. package/dist/types/src/components/ScriptBlock/Splitter/index.d.ts.map +1 -0
  69. package/dist/types/src/components/ScriptBlock/index.d.ts +2 -0
  70. package/dist/types/src/components/ScriptBlock/index.d.ts.map +1 -0
  71. package/dist/types/src/components/ScriptEditor.d.ts +9 -0
  72. package/dist/types/src/components/ScriptEditor.d.ts.map +1 -0
  73. package/dist/types/src/components/Toolbar.d.ts +11 -0
  74. package/dist/types/src/components/Toolbar.d.ts.map +1 -0
  75. package/dist/types/src/components/Toolbar.stories.d.ts +16 -0
  76. package/dist/types/src/components/Toolbar.stories.d.ts.map +1 -0
  77. package/dist/types/src/components/TypescriptEditor.d.ts +10 -0
  78. package/dist/types/src/components/TypescriptEditor.d.ts.map +1 -0
  79. package/dist/types/src/components/TypescriptEditor.stories.d.ts +13 -0
  80. package/dist/types/src/components/TypescriptEditor.stories.d.ts.map +1 -0
  81. package/dist/types/src/components/index.d.ts +4 -0
  82. package/dist/types/src/components/index.d.ts.map +1 -0
  83. package/dist/types/src/edge/codec.d.ts +4 -0
  84. package/dist/types/src/edge/codec.d.ts.map +1 -0
  85. package/dist/types/src/edge/functions.d.ts +23 -0
  86. package/dist/types/src/edge/functions.d.ts.map +1 -0
  87. package/dist/types/src/edge/header-codec.d.ts +24 -0
  88. package/dist/types/src/edge/header-codec.d.ts.map +1 -0
  89. package/dist/types/src/edge/hub-protocol.d.ts +6 -0
  90. package/dist/types/src/edge/hub-protocol.d.ts.map +1 -0
  91. package/dist/types/src/edge/index.d.ts +5 -0
  92. package/dist/types/src/edge/index.d.ts.map +1 -0
  93. package/dist/types/src/index.d.ts +7 -0
  94. package/dist/types/src/index.d.ts.map +1 -0
  95. package/dist/types/src/meta.d.ts +15 -0
  96. package/dist/types/src/meta.d.ts.map +1 -0
  97. package/dist/types/src/translations.d.ts +19 -0
  98. package/dist/types/src/translations.d.ts.map +1 -0
  99. package/dist/types/src/types/index.d.ts +3 -0
  100. package/dist/types/src/types/index.d.ts.map +1 -0
  101. package/dist/types/src/types/script.d.ts +26 -0
  102. package/dist/types/src/types/script.d.ts.map +1 -0
  103. package/dist/types/src/types/types.d.ts +8 -0
  104. package/dist/types/src/types/types.d.ts.map +1 -0
  105. package/package.json +120 -0
  106. package/src/ScriptPlugin.tsx +137 -0
  107. package/src/compiler/compiler.test.ts +17 -0
  108. package/src/compiler/compiler.ts +210 -0
  109. package/src/compiler/index.ts +5 -0
  110. package/src/components/ScriptBlock/FrameContainer/FrameContainer.tsx +82 -0
  111. package/src/components/ScriptBlock/FrameContainer/frame.html +25 -0
  112. package/src/components/ScriptBlock/FrameContainer/frame.tsx +51 -0
  113. package/src/components/ScriptBlock/FrameContainer/index.ts +5 -0
  114. package/src/components/ScriptBlock/ScriptBlock.stories.tsx +64 -0
  115. package/src/components/ScriptBlock/ScriptBlock.tsx +132 -0
  116. package/src/components/ScriptBlock/Splitter/Splitter.stories.tsx +37 -0
  117. package/src/components/ScriptBlock/Splitter/Splitter.tsx +52 -0
  118. package/src/components/ScriptBlock/Splitter/index.ts +5 -0
  119. package/src/components/ScriptBlock/index.ts +5 -0
  120. package/src/components/ScriptEditor.tsx +113 -0
  121. package/src/components/Toolbar.stories.tsx +19 -0
  122. package/src/components/Toolbar.tsx +163 -0
  123. package/src/components/TypescriptEditor.stories.tsx +63 -0
  124. package/src/components/TypescriptEditor.tsx +66 -0
  125. package/src/components/index.ts +8 -0
  126. package/src/edge/codec.ts +13 -0
  127. package/src/edge/functions.ts +128 -0
  128. package/src/edge/header-codec.ts +97 -0
  129. package/src/edge/hub-protocol.ts +27 -0
  130. package/src/edge/index.ts +10 -0
  131. package/src/index.ts +13 -0
  132. package/src/meta.tsx +19 -0
  133. package/src/translations.ts +25 -0
  134. package/src/types/index.ts +6 -0
  135. package/src/types/script.ts +16 -0
  136. package/src/types/types.tsx +27 -0
@@ -0,0 +1,132 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Check, Play, Warning } from '@phosphor-icons/react';
6
+ // @ts-ignore
7
+ import esbuildWasmURL from 'esbuild-wasm/esbuild.wasm?url';
8
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
9
+
10
+ import { createDocAccessor, DocAccessor } from '@dxos/react-client/echo';
11
+ import { DensityProvider, Toolbar, Button } from '@dxos/react-ui';
12
+ import { mx, getSize } from '@dxos/react-ui-theme';
13
+
14
+ import { FrameContainer } from './FrameContainer';
15
+ import { Splitter, SplitterSelector, type View } from './Splitter';
16
+ import { Compiler, type CompilerResult, initializeCompiler } from '../../compiler';
17
+ import { type ScriptType } from '../../types';
18
+ import { ScriptEditor } from '../ScriptEditor';
19
+
20
+ // Keep in sync with packages/apps/composer-app/script-frame/main.tsx .
21
+ const PROVIDED_MODULES = [
22
+ 'react',
23
+ 'react-dom/client',
24
+ '@dxos/client',
25
+ '@dxos/react-client',
26
+ '@dxos/react-client/echo',
27
+ '@dxos/plugin-explorer',
28
+ ];
29
+
30
+ export type ScriptBlockProps = {
31
+ script: ScriptType;
32
+ view?: View;
33
+ hideSelector?: boolean;
34
+ classes?: {
35
+ root?: string;
36
+ toolbar?: string;
37
+ };
38
+
39
+ // Url to the page used to host the script in the iframe.
40
+ containerUrl: string;
41
+ };
42
+
43
+ /**
44
+ * @deprecated
45
+ */
46
+ // TODO(burdon): Cache compiled results in context.
47
+ export const ScriptBlock = ({
48
+ script,
49
+ view: controlledView,
50
+ hideSelector,
51
+ classes,
52
+ containerUrl,
53
+ }: ScriptBlockProps) => {
54
+ const source = useMemo(() => script.source && createDocAccessor(script.source, ['content']), [script.source]);
55
+ const [view, setView] = useState<View>(controlledView ?? 'editor');
56
+ useEffect(() => handleSetView(controlledView ?? 'editor'), [controlledView]);
57
+
58
+ const [result, setResult] = useState<CompilerResult>();
59
+ const compiler = useMemo(() => new Compiler({ platform: 'browser', providedModules: PROVIDED_MODULES }), []);
60
+ useEffect(() => {
61
+ // TODO(burdon): Create useCompiler hook (with initialization).
62
+ void initializeCompiler({ wasmURL: esbuildWasmURL });
63
+ }, []);
64
+ useEffect(() => {
65
+ // TODO(burdon): Throttle and listen for update.
66
+ const t = setTimeout(async () => {
67
+ if (!source) {
68
+ return;
69
+ }
70
+
71
+ const result = await compiler.compile(DocAccessor.getValue(source));
72
+ setResult(result);
73
+ });
74
+
75
+ return () => clearTimeout(t);
76
+ }, [source]);
77
+
78
+ const handleSetView = useCallback(
79
+ (view: View) => {
80
+ setView(view);
81
+ if (!result && view !== 'editor') {
82
+ void handleExec(false);
83
+ }
84
+ },
85
+ [result],
86
+ );
87
+
88
+ const handleExec = useCallback(
89
+ async (auto = true) => {
90
+ if (!source) {
91
+ return;
92
+ }
93
+ const result = await compiler.compile(DocAccessor.getValue(source));
94
+ setResult(result);
95
+ if (auto && view === 'editor') {
96
+ setView('preview');
97
+ }
98
+ },
99
+ [source, view],
100
+ );
101
+
102
+ return (
103
+ <div className={mx('flex flex-col grow overflow-hidden', classes?.root)}>
104
+ {!hideSelector && (
105
+ <DensityProvider density='fine'>
106
+ <Toolbar.Root classNames={mx('mb-2', classes?.toolbar)}>
107
+ <SplitterSelector view={view} onChange={handleSetView} />
108
+ <div className='grow' />
109
+ {result?.bundle && !result?.error && (
110
+ <div title={String(result.error)}>
111
+ <Check className={mx(getSize(5), 'text-green-500')} />
112
+ </div>
113
+ )}
114
+ {result?.error && (
115
+ <div title={String(result.error)}>
116
+ <Warning className={mx(getSize(5), 'text-orange-500')} />
117
+ </div>
118
+ )}
119
+ <Button variant='ghost' onClick={() => handleExec()}>
120
+ <Play className={getSize(5)} />
121
+ </Button>
122
+ </Toolbar.Root>
123
+ </DensityProvider>
124
+ )}
125
+
126
+ <Splitter view={view}>
127
+ <ScriptEditor script={script} />
128
+ {result && <FrameContainer key={script.id} result={result} containerUrl={containerUrl} />}
129
+ </Splitter>
130
+ </div>
131
+ );
132
+ };
@@ -0,0 +1,37 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import React, { useState } from 'react';
8
+
9
+ import { DensityProvider } from '@dxos/react-ui';
10
+
11
+ import { Splitter, SplitterSelector, type View } from './Splitter';
12
+
13
+ const Story = () => {
14
+ const [view, setView] = useState<View>();
15
+ return (
16
+ <div>
17
+ <DensityProvider density='fine'>
18
+ <SplitterSelector view={view} onChange={setView} />
19
+ </DensityProvider>
20
+ <Splitter view={view}>
21
+ <div>1</div>
22
+ <div>2</div>
23
+ </Splitter>
24
+ </div>
25
+ );
26
+ };
27
+
28
+ export default {
29
+ title: 'plugin-script/Splitter',
30
+ component: Splitter,
31
+ render: Story,
32
+ parameters: {
33
+ layout: 'fullscreen',
34
+ },
35
+ };
36
+
37
+ export const Default = {};
@@ -0,0 +1,52 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Code, SquareSplitHorizontal, Eye } from '@phosphor-icons/react';
6
+ import React, { type ReactNode } from 'react';
7
+
8
+ import { ToggleGroup, ToggleGroupItem, Toolbar } from '@dxos/react-ui';
9
+ import { getSize, mx } from '@dxos/react-ui-theme';
10
+
11
+ export type View = 'editor' | 'preview' | 'split';
12
+
13
+ export type SplitterProps = {
14
+ view?: View;
15
+ className?: string;
16
+ children?: ReactNode[];
17
+ };
18
+
19
+ // TODO(burdon): Surfaces?
20
+ export const Splitter = ({ view = 'split', className, children = [] }: SplitterProps) => {
21
+ const [left, right] = children;
22
+ return (
23
+ <div className={mx('flex overflow-hidden grow', className)}>
24
+ {left && view !== 'preview' && <div className='flex flex-1 shrink-0 overflow-hidden'>{left}</div>}
25
+ {right && view !== 'editor' && <div className='flex flex-1 shrink-0 overflow-hidden'>{right}</div>}
26
+ </div>
27
+ );
28
+ };
29
+
30
+ export type SplitterSelectorProps = {
31
+ view?: View;
32
+ onChange?: (view: View) => void;
33
+ className?: string;
34
+ };
35
+
36
+ export const SplitterSelector = ({ view, onChange, className }: SplitterSelectorProps) => {
37
+ return (
38
+ <Toolbar.Root classNames={className}>
39
+ <ToggleGroup type='single' value={view} onValueChange={(value) => onChange?.(value as View)}>
40
+ <ToggleGroupItem value='editor'>
41
+ <Code className={getSize(5)} />
42
+ </ToggleGroupItem>
43
+ <ToggleGroupItem value='split'>
44
+ <SquareSplitHorizontal className={getSize(5)} />
45
+ </ToggleGroupItem>
46
+ <ToggleGroupItem value='preview'>
47
+ <Eye className={getSize(5)} />
48
+ </ToggleGroupItem>
49
+ </ToggleGroup>
50
+ </Toolbar.Root>
51
+ );
52
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export * from './Splitter';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './ScriptBlock';
@@ -0,0 +1,113 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type DID } from 'iso-did/types';
6
+ import React, { useCallback, useMemo, useState } from 'react';
7
+
8
+ import { log } from '@dxos/log';
9
+ import { useClient } from '@dxos/react-client';
10
+ import { createDocAccessor, getMeta, getSpace } from '@dxos/react-client/echo';
11
+ import { useIdentity } from '@dxos/react-client/halo';
12
+ import { useTranslation } from '@dxos/react-ui';
13
+ import { createDataExtensions, listener } from '@dxos/react-ui-editor';
14
+ import { mx } from '@dxos/react-ui-theme';
15
+
16
+ import { Toolbar } from './Toolbar';
17
+ import { TypescriptEditor } from './TypescriptEditor';
18
+ import {
19
+ getUserFunctionUrlInMetadata,
20
+ publicKeyToDid,
21
+ setUserFunctionUrlInMetadata,
22
+ uploadWorkerFunction,
23
+ } from '../edge';
24
+ import { SCRIPT_PLUGIN } from '../meta';
25
+ import { type ScriptType } from '../types';
26
+
27
+ export type ScriptEditorProps = {
28
+ script: ScriptType;
29
+ role?: string;
30
+ };
31
+
32
+ export const ScriptEditor = ({ script, role }: ScriptEditorProps) => {
33
+ const { t } = useTranslation(SCRIPT_PLUGIN);
34
+ const client = useClient();
35
+ const identity = useIdentity();
36
+ const space = getSpace(script);
37
+ const extensions = useMemo(
38
+ () => [
39
+ listener({
40
+ onChange: (text) => {
41
+ if (script.source && script.source?.content !== text) {
42
+ script.changed = true;
43
+ }
44
+ },
45
+ }),
46
+ createDataExtensions({
47
+ id: script.id,
48
+ text: script.source && createDocAccessor(script.source, ['content']),
49
+ space,
50
+ identity,
51
+ }),
52
+ ],
53
+ [script, script.source, space, identity],
54
+ );
55
+ const initialValue = useMemo(() => script.source?.content, [script.source]);
56
+ const existingFunctionUrl = getUserFunctionUrlInMetadata(getMeta(script));
57
+ const [error, setError] = useState<string>();
58
+
59
+ const handleBindingChange = useCallback(
60
+ (binding: string) => {
61
+ script.binding = binding;
62
+ },
63
+ [script],
64
+ );
65
+
66
+ const handleDeploy = useCallback(async () => {
67
+ if (!script.source || !identity) {
68
+ return;
69
+ }
70
+
71
+ setError(undefined);
72
+
73
+ try {
74
+ const existingFunctionId = existingFunctionUrl?.split('/').at(-1);
75
+ const ownerDid = (existingFunctionUrl?.split('/').at(-2) as DID) ?? publicKeyToDid(identity.identityKey);
76
+
77
+ const { result, functionId, functionVersionNumber, errorMessage } = await uploadWorkerFunction({
78
+ clientConfig: client.config,
79
+ halo: client.halo,
80
+ ownerDid,
81
+ functionId: existingFunctionId,
82
+ name: script.name,
83
+ source: script.source.content,
84
+ });
85
+ if (result !== 'success' || functionId === undefined) {
86
+ throw new Error(errorMessage);
87
+ }
88
+ log.info('function uploaded', { functionId, functionVersionNumber });
89
+ const meta = getMeta(script);
90
+ setUserFunctionUrlInMetadata(meta, `/${ownerDid}/${functionId}`);
91
+ script.changed = false;
92
+ } catch (err: any) {
93
+ log.catch(err);
94
+ setError(t('upload failed label'));
95
+ }
96
+ }, [existingFunctionUrl, script, script.name, script.source]);
97
+
98
+ return (
99
+ <div role='none' className={mx(role === 'article' && 'row-span-2', 'is-full pli-2')}>
100
+ <Toolbar
101
+ binding={script.binding ?? ''}
102
+ onBindingChange={handleBindingChange}
103
+ deployed={Boolean(existingFunctionUrl) && !script.changed}
104
+ onDeploy={handleDeploy}
105
+ functionUrl={existingFunctionUrl}
106
+ error={error}
107
+ />
108
+ <TypescriptEditor id={script.id} initialValue={initialValue} extensions={extensions} />
109
+ </div>
110
+ );
111
+ };
112
+
113
+ export default ScriptEditor;
@@ -0,0 +1,19 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ // import React from 'react';
8
+
9
+ import { Toolbar } from './Toolbar';
10
+
11
+ export default {
12
+ title: 'plugin-script/Toolbar',
13
+ component: Toolbar,
14
+ parameters: {
15
+ layout: 'fullscreen',
16
+ },
17
+ };
18
+
19
+ export const Default = { args: { binding: 'example', deployed: true } };
@@ -0,0 +1,163 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { Check, Checks, CircleNotch, Link, PencilSimple, UploadSimple, WarningCircle, X } from '@phosphor-icons/react';
6
+ import React, { useRef, useState } from 'react';
7
+
8
+ import {
9
+ Button,
10
+ DensityProvider,
11
+ ElevationProvider,
12
+ Input,
13
+ Toolbar as NaturalToolbar,
14
+ Tooltip,
15
+ useTranslation,
16
+ } from '@dxos/react-ui';
17
+ import { getSize, mx } from '@dxos/react-ui-theme';
18
+
19
+ import { SCRIPT_PLUGIN } from '../meta';
20
+
21
+ export type ToolbarProps = {
22
+ binding: string;
23
+ onBindingChange: (binding: string) => void;
24
+ deployed?: boolean;
25
+ onDeploy?: () => Promise<void>;
26
+ functionUrl?: string;
27
+ error?: string;
28
+ };
29
+
30
+ export const Toolbar = ({ binding, onBindingChange, deployed, onDeploy, functionUrl, error }: ToolbarProps) => {
31
+ const { t } = useTranslation(SCRIPT_PLUGIN);
32
+ const [pending, setPending] = useState(false);
33
+ const [editing, setEditing] = useState(false);
34
+ const inputRef = useRef<HTMLInputElement>(null);
35
+
36
+ const handleEdit = () => {
37
+ setEditing(true);
38
+ inputRef.current?.focus();
39
+ };
40
+
41
+ const handleSave = () => {
42
+ setEditing(false);
43
+ onBindingChange(inputRef.current?.value || '');
44
+ };
45
+
46
+ const handleCancel = () => {
47
+ setEditing(false);
48
+ };
49
+
50
+ const handleCopyLink = async () => {
51
+ if (!functionUrl) {
52
+ return;
53
+ }
54
+
55
+ await navigator.clipboard.writeText(functionUrl);
56
+ };
57
+
58
+ const handleDeploy = async () => {
59
+ setPending(true);
60
+ await onDeploy?.();
61
+ setPending(false);
62
+ };
63
+
64
+ return (
65
+ <DensityProvider density='fine'>
66
+ <ElevationProvider elevation='chrome'>
67
+ <NaturalToolbar.Root
68
+ classNames='p-1 is-full shrink-0 overflow-x-auto overflow-y-hidden'
69
+ style={{ contain: 'layout' }}
70
+ >
71
+ {/* TODO(wittjosiah): Binding input shouldn't be in the toolbar. */}
72
+ <Input.Root>
73
+ <Input.Label>{t('binding label')}</Input.Label>
74
+ <Input.TextInput ref={inputRef} defaultValue={binding} disabled={!editing} classNames='!is-auto' />
75
+ {editing ? (
76
+ <>
77
+ <Button variant='ghost' onClick={handleCancel}>
78
+ <X className={getSize(4)} />
79
+ </Button>
80
+ <Button variant='ghost' onClick={handleSave}>
81
+ <Check className={getSize(4)} />
82
+ </Button>
83
+ </>
84
+ ) : (
85
+ <Tooltip.Root>
86
+ <Tooltip.Trigger asChild>
87
+ <Button variant='ghost' onClick={handleEdit}>
88
+ <PencilSimple className={getSize(4)} />
89
+ </Button>
90
+ </Tooltip.Trigger>
91
+ <Tooltip.Portal>
92
+ <Tooltip.Content>
93
+ <Tooltip.Arrow />
94
+ {t('edit binding label')}
95
+ </Tooltip.Content>
96
+ </Tooltip.Portal>
97
+ </Tooltip.Root>
98
+ )}
99
+ </Input.Root>
100
+ <div role='separator' className='grow' />
101
+ {/* TODO(wittjosiah): Better treatment for status indicators in toolbar? */}
102
+ {error ? (
103
+ <Tooltip.Root>
104
+ <Tooltip.Trigger>
105
+ <WarningCircle className={getSize(4)} />
106
+ </Tooltip.Trigger>
107
+ <Tooltip.Portal>
108
+ <Tooltip.Content>
109
+ <Tooltip.Arrow />
110
+ {error}
111
+ </Tooltip.Content>
112
+ </Tooltip.Portal>
113
+ </Tooltip.Root>
114
+ ) : deployed ? (
115
+ <Tooltip.Root>
116
+ <Tooltip.Trigger>
117
+ <Checks className={getSize(4)} />
118
+ </Tooltip.Trigger>
119
+ <Tooltip.Portal>
120
+ <Tooltip.Content>
121
+ <Tooltip.Arrow />
122
+ {t('deployed label')}
123
+ </Tooltip.Content>
124
+ </Tooltip.Portal>
125
+ </Tooltip.Root>
126
+ ) : null}
127
+ {functionUrl && (
128
+ <Tooltip.Root>
129
+ <Tooltip.Trigger asChild>
130
+ <NaturalToolbar.Button variant='ghost' onClick={handleCopyLink}>
131
+ <Link className={getSize(4)} />
132
+ </NaturalToolbar.Button>
133
+ </Tooltip.Trigger>
134
+ <Tooltip.Portal>
135
+ <Tooltip.Content>
136
+ <Tooltip.Arrow />
137
+ {t('copy link label')}
138
+ </Tooltip.Content>
139
+ </Tooltip.Portal>
140
+ </Tooltip.Root>
141
+ )}
142
+ <Tooltip.Root>
143
+ <Tooltip.Trigger asChild>
144
+ <NaturalToolbar.Button variant='ghost' onClick={handleDeploy} disabled={pending}>
145
+ {pending ? (
146
+ <CircleNotch className={mx('spinner', getSize(4))} />
147
+ ) : (
148
+ <UploadSimple className={getSize(4)} />
149
+ )}
150
+ </NaturalToolbar.Button>
151
+ </Tooltip.Trigger>
152
+ <Tooltip.Portal>
153
+ <Tooltip.Content>
154
+ <Tooltip.Arrow />
155
+ {pending ? t('pending label') : t('deploy label')}
156
+ </Tooltip.Content>
157
+ </Tooltip.Portal>
158
+ </Tooltip.Root>
159
+ </NaturalToolbar.Root>
160
+ </ElevationProvider>
161
+ </DensityProvider>
162
+ );
163
+ };
@@ -0,0 +1,63 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ // TODO(burdon): Typescript.
6
+ // TODO(burdon): Effect schema.
7
+ // TODO(burdon): JSX.
8
+ // TODO(burdon): react-buddy for storybook?
9
+
10
+ // Marijn
11
+ // - language support for S
12
+ // - hierarchical editor (DND)
13
+ // - virtual document image rendering
14
+ // - mobile rendering error
15
+
16
+ import '@dxosTheme';
17
+
18
+ import React, { useMemo } from 'react';
19
+
20
+ import { createDocAccessor, createEchoObject } from '@dxos/react-client/echo';
21
+ import { createDataExtensions } from '@dxos/react-ui-editor';
22
+
23
+ import { TypescriptEditor } from './TypescriptEditor';
24
+
25
+ const examples: string[] = [
26
+ [
27
+ '// Example function.',
28
+ 'export default function() {',
29
+ ' const value = 100',
30
+ ' return <div>{value}</div>;',
31
+ '}',
32
+ ].join('\n'),
33
+ [
34
+ '// Example schema.',
35
+ 'S.Struct({',
36
+ ' timestamp: S.Date,',
37
+ ' title: S.String,',
38
+ ' content: R.Text,',
39
+ "}).pipe(S.identifier('dxos.org/schema/Test'))",
40
+ '',
41
+ ].join('\n'),
42
+ ];
43
+
44
+ const Story = () => {
45
+ const object = useMemo(() => createEchoObject({ content: examples[0] }), []);
46
+ const initialValue = useMemo(() => object.content, [object]);
47
+ const accessor = useMemo(() => createDocAccessor(object, ['content']), [object]);
48
+ const extensions = useMemo(() => [createDataExtensions({ id: object.id, text: accessor })], [object.id, accessor]);
49
+
50
+ // TODO(wittjosiah): Scroll past end breaks the editor.
51
+ return <TypescriptEditor id='test' initialValue={initialValue} extensions={extensions} scrollPastEnd={false} />;
52
+ };
53
+
54
+ export default {
55
+ title: 'plugin-script/TypescriptEditor',
56
+ component: TypescriptEditor,
57
+ render: Story,
58
+ parameters: {
59
+ layout: 'fullscreen',
60
+ },
61
+ };
62
+
63
+ export const Default = {};
@@ -0,0 +1,66 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { javascript } from '@codemirror/lang-javascript';
6
+ import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
7
+ import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
8
+ import React from 'react';
9
+
10
+ import { useThemeContext } from '@dxos/react-ui';
11
+ import {
12
+ autocomplete,
13
+ createBasicExtensions,
14
+ createThemeExtensions,
15
+ editorContent,
16
+ useTextEditor,
17
+ type UseTextEditorProps,
18
+ } from '@dxos/react-ui-editor';
19
+ import { nonNullable } from '@dxos/util';
20
+
21
+ export type TypescriptEditorProps = {
22
+ id: string;
23
+ className?: string;
24
+ scrollPastEnd?: boolean;
25
+ } & Pick<UseTextEditorProps, 'initialValue' | 'extensions' | 'scrollTo' | 'selection'>;
26
+
27
+ export const TypescriptEditor = ({
28
+ id,
29
+ extensions,
30
+ initialValue,
31
+ scrollTo,
32
+ selection,
33
+ className,
34
+ scrollPastEnd,
35
+ }: TypescriptEditorProps) => {
36
+ const { themeMode } = useThemeContext();
37
+
38
+ const { parentRef, focusAttributes } = useTextEditor(
39
+ () => ({
40
+ id,
41
+ initialValue,
42
+ extensions: [
43
+ extensions,
44
+ // TODO(wittjosiah): Highlight active line doesn't work.
45
+ createBasicExtensions({ highlightActiveLine: true, indentWithTab: true, lineNumbers: true, scrollPastEnd }),
46
+ // TODO(wittjosiah): Factor out syntax highlighting to theme extensions.
47
+ themeMode === 'dark' ? syntaxHighlighting(oneDarkHighlightStyle) : syntaxHighlighting(defaultHighlightStyle),
48
+ createThemeExtensions({
49
+ themeMode,
50
+ slots: {
51
+ content: { className: editorContent },
52
+ },
53
+ }),
54
+ javascript({ typescript: true }),
55
+ autocomplete(),
56
+ ].filter(nonNullable),
57
+ selection,
58
+ scrollTo,
59
+ }),
60
+ [id, initialValue, extensions, themeMode, selection, scrollTo],
61
+ );
62
+
63
+ return <div ref={parentRef} className={className} {...focusAttributes} />;
64
+ };
65
+
66
+ export default TypescriptEditor;
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ export const ScriptEditor = React.lazy(() => import('./ScriptEditor'));
8
+ export const TypescriptEditor = React.lazy(() => import('./TypescriptEditor'));
@@ -0,0 +1,13 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { PublicKey } from '@dxos/keys';
6
+
7
+ import { HeaderCodec, type EncodedKey } from './header-codec';
8
+
9
+ export const codec = new HeaderCodec<PublicKey>({
10
+ isKey: (value: any): value is PublicKey => value instanceof PublicKey,
11
+ encode: (key: PublicKey): EncodedKey => ({ __k: key.toHex() }),
12
+ decode: (value: EncodedKey): PublicKey => PublicKey.fromHex(value.__k),
13
+ });