From 37fc5c61f19abad7a83a11c61792a5c3874a4aed Mon Sep 17 00:00:00 2001 From: fantonangeli Date: Thu, 3 Sep 2026 14:57:00 +0200 Subject: [PATCH 1/7] Add text-editor package Signed-off-by: fantonangeli --- .changeset/add-text-editor-package.md | 5 + packages/text-editor/.oxfmtrc.json | 3 + packages/text-editor/.oxlintrc.json | 3 + packages/text-editor/.storybook/main.ts | 36 ++++ packages/text-editor/.storybook/preview.tsx | 71 +++++++ packages/text-editor/README.md | 58 ++++++ packages/text-editor/package.json | 78 +++++++ packages/text-editor/playwright.config.ts | 36 ++++ packages/text-editor/src/TextEditor.tsx | 110 ++++++++++ packages/text-editor/src/index.ts | 16 ++ .../stories/features/TextEditor.stories.tsx | 55 +++++ .../stories/features/TextEditor.tsx | 31 +++ packages/text-editor/stories/helpers.ts | 35 ++++ .../stories/introduction/Welcome.mdx | 44 ++++ .../stories/samples/hello-world.json | 19 ++ .../stories/samples/hello-world.yaml | 26 +++ packages/text-editor/stories/samples/index.ts | 18 ++ .../text-editor/tests-e2e/text-editor.spec.ts | 29 +++ .../tests/__mocks__/monaco-editor.ts | 80 ++++++++ packages/text-editor/tests/setupTests.ts | 21 ++ .../text-editor/TextEditor.story.test.tsx | 50 +++++ .../tests/text-editor/TextEditor.test.tsx | 193 ++++++++++++++++++ packages/text-editor/tsconfig.json | 18 ++ packages/text-editor/tsconfig.test.json | 22 ++ packages/text-editor/vite.config.ts | 34 +++ packages/text-editor/vitest.config.ts | 61 ++++++ pnpm-lock.yaml | 116 +++++++++++ pnpm-workspace.yaml | 1 + 28 files changed, 1269 insertions(+) create mode 100644 .changeset/add-text-editor-package.md create mode 100644 packages/text-editor/.oxfmtrc.json create mode 100644 packages/text-editor/.oxlintrc.json create mode 100644 packages/text-editor/.storybook/main.ts create mode 100644 packages/text-editor/.storybook/preview.tsx create mode 100644 packages/text-editor/README.md create mode 100644 packages/text-editor/package.json create mode 100644 packages/text-editor/playwright.config.ts create mode 100644 packages/text-editor/src/TextEditor.tsx create mode 100644 packages/text-editor/src/index.ts create mode 100644 packages/text-editor/stories/features/TextEditor.stories.tsx create mode 100644 packages/text-editor/stories/features/TextEditor.tsx create mode 100644 packages/text-editor/stories/helpers.ts create mode 100644 packages/text-editor/stories/introduction/Welcome.mdx create mode 100644 packages/text-editor/stories/samples/hello-world.json create mode 100644 packages/text-editor/stories/samples/hello-world.yaml create mode 100644 packages/text-editor/stories/samples/index.ts create mode 100644 packages/text-editor/tests-e2e/text-editor.spec.ts create mode 100644 packages/text-editor/tests/__mocks__/monaco-editor.ts create mode 100644 packages/text-editor/tests/setupTests.ts create mode 100644 packages/text-editor/tests/text-editor/TextEditor.story.test.tsx create mode 100644 packages/text-editor/tests/text-editor/TextEditor.test.tsx create mode 100644 packages/text-editor/tsconfig.json create mode 100644 packages/text-editor/tsconfig.test.json create mode 100644 packages/text-editor/vite.config.ts create mode 100644 packages/text-editor/vitest.config.ts diff --git a/.changeset/add-text-editor-package.md b/.changeset/add-text-editor-package.md new file mode 100644 index 00000000..792d000d --- /dev/null +++ b/.changeset/add-text-editor-package.md @@ -0,0 +1,5 @@ +--- +"@openworkflowspec/text-editor": minor +--- + +Add a Monaco-based controlled React Text Editor for JSON and YAML Open Workflow documents. diff --git a/packages/text-editor/.oxfmtrc.json b/packages/text-editor/.oxfmtrc.json new file mode 100644 index 00000000..0de643fc --- /dev/null +++ b/packages/text-editor/.oxfmtrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../../.oxfmtrc.json"] +} diff --git a/packages/text-editor/.oxlintrc.json b/packages/text-editor/.oxlintrc.json new file mode 100644 index 00000000..804b9227 --- /dev/null +++ b/packages/text-editor/.oxlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["../../.oxlintrc.json"] +} diff --git a/packages/text-editor/.storybook/main.ts b/packages/text-editor/.storybook/main.ts new file mode 100644 index 00000000..a4511bb5 --- /dev/null +++ b/packages/text-editor/.storybook/main.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { StorybookConfig } from "@storybook/react-vite"; + +const config: StorybookConfig = { + typescript: { + check: true, + }, + core: { + disableTelemetry: true, // Do not collect data + }, + stories: ["../stories/**/*.mdx", "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + addons: [ + "@chromatic-com/storybook", + "@storybook/addon-vitest", + "@storybook/addon-a11y", + "@storybook/addon-docs", + ], + framework: "@storybook/react-vite", +}; + +export default config; diff --git a/packages/text-editor/.storybook/preview.tsx b/packages/text-editor/.storybook/preview.tsx new file mode 100644 index 00000000..5be65130 --- /dev/null +++ b/packages/text-editor/.storybook/preview.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Preview, Decorator } from "@storybook/react-vite"; + +const withColorMode: Decorator = (Story) => { + return ; +}; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + disableSaveFromUI: true, // Disable modify story popup. Stories mustn't be editable from Storybook UI. + }, + + backgrounds: { + disable: true, + }, + + a11y: { + // 'todo' - show a11y violations in the test UI only + // 'error' - fail CI on a11y violations + // 'off' - skip a11y checks entirely + test: "todo", + }, + + options: { + storySort: { + order: ["Introduction", "Features"], + }, + }, + }, + + globalTypes: { + colorMode: { + description: "Global color mode for components", + defaultValue: "system", + toolbar: { + title: "Color Mode", + icon: "circlehollow", + items: [ + { value: "light", icon: "sun", title: "Light" }, + { value: "dark", icon: "moon", title: "Dark" }, + { value: "system", icon: "browser", title: "System" }, + ], + dynamicTitle: true, + }, + }, + }, + + decorators: [withColorMode], +}; + +export default preview; diff --git a/packages/text-editor/README.md b/packages/text-editor/README.md new file mode 100644 index 00000000..5a7d4d53 --- /dev/null +++ b/packages/text-editor/README.md @@ -0,0 +1,58 @@ + + +# @openworkflowspec/text-editor + +React text editor component for Open Workflow documents, based on [Monaco Editor](https://github.com/microsoft/monaco-editor). + +## Overview + +`TextEditor` is a controlled component that provides: + +- JSON and YAML syntax highlighting; +- read-only mode; +- controlled content updates; +- language-service features, such as completions and diagnostics, provided by `@openworkflowspec/language-service`. + +## Props + +| Prop | Type | Required | Default | Description | +| ----------------- | --------------------------- | -------- | ----------- | ------------------------------------------- | +| `content` | `string` | ✅ | — | Current document content. | +| `language` | `TextEditorLanguage` | ✅ | — | Document language: `json` or `yaml`. | +| `isReadOnly` | `boolean` | — | `false` | Prevents editing when enabled. | +| `onContentChange` | `(content: string) => void` | — | `undefined` | Called when the user modifies the document. | + +## Sizing + +The editor fills `100%` of its container's width and height. The host must provide a container with a non-zero height. + +## Usage + +```tsx +import { TextEditor } from "@openworkflowspec/text-editor"; +import { useState } from "react"; + +function App() { + const [content, setContent] = useState('{"hello": "world"}'); + + return ( +
+ +
+ ); +} +``` diff --git a/packages/text-editor/package.json b/packages/text-editor/package.json new file mode 100644 index 00000000..545e6891 --- /dev/null +++ b/packages/text-editor/package.json @@ -0,0 +1,78 @@ +{ + "name": "@openworkflowspec/text-editor", + "version": "1.1.0", + "private": true, + "description": "React Open Workflow text editor component backed by Monaco", + "keywords": [], + "homepage": "https://github.com/open-workflow-specification/editor", + "bugs": { + "url": "https://github.com/open-workflow-specification/editor/issues" + }, + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/open-workflow-specification/editor.git" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "oxlint --config .oxlintrc.json src/ stories/ tests/", + "format": "oxfmt --config .oxfmtrc.json", + "format:check": "oxfmt --config .oxfmtrc.json --check", + "clean": "rimraf ./dist", + "clean:storybook": "rimraf ./dist-storybook", + "build:dev": "pnpm clean && tsc -p tsconfig.json && vite build", + "build:prod": "pnpm lint && pnpm clean && tsc -p tsconfig.json && vite build && pnpm test && pnpm test-e2e", + "test": "vitest run", + "start": "storybook dev -p 6007 --no-open", + "build:storybook": "pnpm clean:storybook && storybook build --output-dir ./dist-storybook", + "test-e2e": "playwright test", + "test-e2e:ui": "playwright test --ui" + }, + "dependencies": { + "monaco-editor": "catalog:" + }, + "devDependencies": { + "@chromatic-com/storybook": "catalog:", + "@playwright/test": "catalog:", + "@storybook/addon-a11y": "catalog:", + "@storybook/addon-docs": "catalog:", + "@storybook/addon-vitest": "catalog:", + "@storybook/react-vite": "catalog:", + "@testing-library/dom": "catalog:", + "@testing-library/jest-dom": "catalog:", + "@testing-library/react": "catalog:", + "@testing-library/user-event": "catalog:", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "@vitest/browser": "catalog:", + "@vitest/browser-playwright": "catalog:", + "@vitest/coverage-v8": "catalog:", + "@vitest/ui": "catalog:", + "jsdom": "catalog:", + "oxfmt": "catalog:", + "oxlint": "catalog:", + "react": "catalog:", + "react-dom": "catalog:", + "rimraf": "catalog:", + "storybook": "catalog:", + "vite": "catalog:", + "vitest": "catalog:" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } +} diff --git a/packages/text-editor/playwright.config.ts b/packages/text-editor/playwright.config.ts new file mode 100644 index 00000000..768e00bc --- /dev/null +++ b/packages/text-editor/playwright.config.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "tests-e2e", + + expect: { + timeout: 30000, + }, + + use: { + baseURL: "http://localhost:6007", + }, + + webServer: { + command: "pnpm start", + url: "http://localhost:6007", + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); diff --git a/packages/text-editor/src/TextEditor.tsx b/packages/text-editor/src/TextEditor.tsx new file mode 100644 index 00000000..f1fa0b81 --- /dev/null +++ b/packages/text-editor/src/TextEditor.tsx @@ -0,0 +1,110 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as React from "react"; +import * as monaco from "monaco-editor"; + +export type TextEditorLanguage = "json" | "yaml"; + +export type TextEditorProps = { + content: string; + language: TextEditorLanguage; + onContentChange?: (content: string) => void; + isReadOnly?: boolean; +}; + +export const TextEditor = ({ + content, + language, + onContentChange, + isReadOnly = false, +}: TextEditorProps) => { + const containerRef = React.useRef(null); + const editorRef = React.useRef(null); + const isApplyingExternalContentRef = React.useRef(false); + + React.useEffect(() => { + if (!containerRef.current) { + return; + } + + const editor = monaco.editor.create(containerRef.current, { + value: content, + language, + readOnly: isReadOnly, + automaticLayout: true, + renderLineHighlight: "none", + }); + + editorRef.current = editor; + + return () => { + editor.dispose(); + editorRef.current = null; + }; + + // Monaco must be created only once. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + React.useEffect(() => { + const editor = editorRef.current; + + if (!editor || !onContentChange) { + return; + } + + const disposable = editor.onDidChangeModelContent(() => { + if (!isApplyingExternalContentRef.current) { + onContentChange(editor.getValue()); + } + }); + + return () => disposable.dispose(); + }, [onContentChange]); + + React.useEffect(() => { + const editor = editorRef.current; + + if (!editor || editor.getValue() === content) { + return; + } + + isApplyingExternalContentRef.current = true; + editor.setValue(content); + isApplyingExternalContentRef.current = false; + }, [content]); + + React.useEffect(() => { + const model = editorRef.current?.getModel(); + + if (model && model.getLanguageId() !== language) { + monaco.editor.setModelLanguage(model, language); + } + }, [language]); + + React.useEffect(() => { + editorRef.current?.updateOptions({ readOnly: isReadOnly }); + }, [isReadOnly]); + + return ( +
+ ); +}; diff --git a/packages/text-editor/src/index.ts b/packages/text-editor/src/index.ts new file mode 100644 index 00000000..a9458386 --- /dev/null +++ b/packages/text-editor/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from "./TextEditor"; diff --git a/packages/text-editor/stories/features/TextEditor.stories.tsx b/packages/text-editor/stories/features/TextEditor.stories.tsx new file mode 100644 index 00000000..43fc6d67 --- /dev/null +++ b/packages/text-editor/stories/features/TextEditor.stories.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { createTextEditorStory } from "../helpers"; +import { helloWorldJson, helloWorldYaml } from "../samples"; +import { TextEditor } from "./TextEditor"; + +const meta = { + id: "text-editor", + title: "Features/Text-Editor", + component: TextEditor, + tags: ["autodocs"], + parameters: { + layout: "fullscreen", + }, + render: (args) => { + return ; + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** JSON document with syntax highlighting. */ +export const JsonEditor: Story = createTextEditorStory({ + content: helloWorldJson, + language: "json", +}); + +/** YAML document with syntax highlighting. */ +export const YamlEditor: Story = createTextEditorStory({ + content: helloWorldYaml, + language: "yaml", +}); + +/** Editor in read-only mode. */ +export const ReadOnly: Story = createTextEditorStory({ + content: helloWorldYaml, + language: "yaml", + isReadOnly: true, +}); diff --git a/packages/text-editor/stories/features/TextEditor.tsx b/packages/text-editor/stories/features/TextEditor.tsx new file mode 100644 index 00000000..03c07c56 --- /dev/null +++ b/packages/text-editor/stories/features/TextEditor.tsx @@ -0,0 +1,31 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TextEditor as Component, TextEditorProps } from "../../src/TextEditor"; + +/** Primary UI component for user interaction */ +export const TextEditor = ({ ...props }: TextEditorProps) => { + return ( +
+ +
+ ); +}; diff --git a/packages/text-editor/stories/helpers.ts b/packages/text-editor/stories/helpers.ts new file mode 100644 index 00000000..4f73df66 --- /dev/null +++ b/packages/text-editor/stories/helpers.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { StoryObj } from "@storybook/react-vite"; +import type { TextEditor } from "./features/TextEditor"; + +type Story = StoryObj; + +/** + * Creates a text editor story with sensible defaults. + * + * @param args - Partial TextEditor props to apply to the story + * @returns A configured Story object + */ +export const createTextEditorStory = (args: Partial[0]>): Story => { + return { + args: { + isReadOnly: false, + ...args, + }, + }; +}; diff --git a/packages/text-editor/stories/introduction/Welcome.mdx b/packages/text-editor/stories/introduction/Welcome.mdx new file mode 100644 index 00000000..c94c0625 --- /dev/null +++ b/packages/text-editor/stories/introduction/Welcome.mdx @@ -0,0 +1,44 @@ +{/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */} + +import { Meta } from "@storybook/addon-docs/blocks"; + + + +# Welcome to the Text Editor + +The **Open Workflow Text Editor** is an embeddable React component for editing [Open Workflow Specification](https://open-workflow-specification.org) documents. + +It is powered by [Monaco Editor](https://github.com/microsoft/monaco-editor) and supports: + +- JSON and YAML syntax highlighting; +- editable and read-only modes; +- controlled content updates; +- Monaco language features such as completions and diagnostics. + +## Get started + +Explore the stories in the **Features** section to see the editor in action. + +The editor fills its parent container, which must have a non-zero height: + +```tsx +
+ +
+``` + +For more information, visit the [GitHub repository](https://github.com/open-workflow-specification/editor). diff --git a/packages/text-editor/stories/samples/hello-world.json b/packages/text-editor/stories/samples/hello-world.json new file mode 100644 index 00000000..a75465a3 --- /dev/null +++ b/packages/text-editor/stories/samples/hello-world.json @@ -0,0 +1,19 @@ +{ + "document": { + "dsl": "1.0.3", + "namespace": "examples", + "name": "hello-world", + "version": "0.1.0" + }, + "do": [ + { + "greet": { + "call": "http", + "with": { + "method": "GET", + "endpoint": "https://httpbin.org/get" + } + } + } + ] +} diff --git a/packages/text-editor/stories/samples/hello-world.yaml b/packages/text-editor/stories/samples/hello-world.yaml new file mode 100644 index 00000000..7ac1964b --- /dev/null +++ b/packages/text-editor/stories/samples/hello-world.yaml @@ -0,0 +1,26 @@ +# +# Copyright 2021-Present The Open Workflow Specification Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +document: + dsl: "1.0.3" + namespace: examples + name: hello-world + version: "0.1.0" +do: + - greet: + call: http + with: + method: GET + endpoint: https://httpbin.org/get diff --git a/packages/text-editor/stories/samples/index.ts b/packages/text-editor/stories/samples/index.ts new file mode 100644 index 00000000..4c86fc39 --- /dev/null +++ b/packages/text-editor/stories/samples/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default as helloWorldJson } from "./hello-world.json?raw"; +export { default as helloWorldYaml } from "./hello-world.yaml?raw"; diff --git a/packages/text-editor/tests-e2e/text-editor.spec.ts b/packages/text-editor/tests-e2e/text-editor.spec.ts new file mode 100644 index 00000000..bb4ebcb7 --- /dev/null +++ b/packages/text-editor/tests-e2e/text-editor.spec.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from "@playwright/test"; + +test("Monaco editor renders and is interactive", async ({ page }) => { + await page.goto("/iframe.html?id=text-editor--json-editor"); + + // Monaco wraps its content in a div with class .monaco-editor + const monacoContainer = page.locator(".monaco-editor").first(); + await expect(monacoContainer).toBeVisible(); + + // Monaco always renders an internal textarea for keyboard interaction + const monacoTextarea = page.locator(".monaco-editor textarea").first(); + await expect(monacoTextarea).toBeAttached(); +}); diff --git a/packages/text-editor/tests/__mocks__/monaco-editor.ts b/packages/text-editor/tests/__mocks__/monaco-editor.ts new file mode 100644 index 00000000..245b1d01 --- /dev/null +++ b/packages/text-editor/tests/__mocks__/monaco-editor.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { vi } from "vitest"; + +type ContentChangeListener = () => void; + +const state = { + value: "", + language: "json", + listener: undefined as ContentChangeListener | undefined, +}; + +export const mockModel = { + getLanguageId: () => state.language, +}; + +export const mockEditorSetValue = vi.fn((value: string) => { + state.value = value; + state.listener?.(); +}); + +export const mockEditorUpdateOptions = vi.fn(); +export const mockEditorDispose = vi.fn(); + +const mockEditor = { + getModel: () => mockModel, + getValue: () => state.value, + setValue: mockEditorSetValue, + updateOptions: mockEditorUpdateOptions, + dispose: mockEditorDispose, + onDidChangeModelContent: (listener: ContentChangeListener) => { + state.listener = listener; + + return { + dispose: () => { + state.listener = undefined; + }, + }; + }, +}; + +export const mockEditorCreate = vi.fn( + (_container: HTMLElement, options: { value?: string; language?: string }) => { + state.value = options.value ?? ""; + state.language = options.language ?? "json"; + state.listener = undefined; + + return mockEditor; + }, +); + +export const mockSetModelLanguage = vi.fn((_model: typeof mockModel, language: string) => { + state.language = language; +}); + +export const simulateEditorContentChange = (value: string) => { + state.value = value; + state.listener?.(); +}; + +export default { + editor: { + create: mockEditorCreate, + setModelLanguage: mockSetModelLanguage, + }, +}; diff --git a/packages/text-editor/tests/setupTests.ts b/packages/text-editor/tests/setupTests.ts new file mode 100644 index 00000000..1dd23b59 --- /dev/null +++ b/packages/text-editor/tests/setupTests.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { cleanup } from "@testing-library/react"; +import "@testing-library/jest-dom/vitest"; +import { afterEach } from "vitest"; + +afterEach(cleanup); diff --git a/packages/text-editor/tests/text-editor/TextEditor.story.test.tsx b/packages/text-editor/tests/text-editor/TextEditor.story.test.tsx new file mode 100644 index 00000000..f7fcc141 --- /dev/null +++ b/packages/text-editor/tests/text-editor/TextEditor.story.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render, screen } from "@testing-library/react"; +import { composeStories } from "@storybook/react-vite"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("monaco-editor", async () => { + const { default: monacoMock } = await import("../__mocks__/monaco-editor"); + return monacoMock; +}); + +import * as stories from "../../stories/features/TextEditor.stories"; +import { mockEditorCreate } from "../__mocks__/monaco-editor"; + +const { JsonEditor, YamlEditor, ReadOnly } = composeStories(stories); + +describe("Story - TextEditor component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it.each([ + { name: "JSON", Story: JsonEditor, language: "json", readOnly: false }, + { name: "YAML", Story: YamlEditor, language: "yaml", readOnly: false }, + { name: "read-only", Story: ReadOnly, language: "yaml", readOnly: true }, + ])("configures the $name story", ({ Story, language, readOnly }) => { + render(); + + const editorContainer = screen.getByTestId("text-editor-container"); + + expect(mockEditorCreate).toHaveBeenCalledWith( + editorContainer, + expect.objectContaining({ language, readOnly }), + ); + }); +}); diff --git a/packages/text-editor/tests/text-editor/TextEditor.test.tsx b/packages/text-editor/tests/text-editor/TextEditor.test.tsx new file mode 100644 index 00000000..ae3f7606 --- /dev/null +++ b/packages/text-editor/tests/text-editor/TextEditor.test.tsx @@ -0,0 +1,193 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { render } from "@testing-library/react"; +import * as React from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("monaco-editor", async () => { + const { default: monacoMock } = await import("../__mocks__/monaco-editor"); + return monacoMock; +}); + +import { + mockEditorCreate, + mockEditorDispose, + mockEditorSetValue, + mockEditorUpdateOptions, + mockModel, + mockSetModelLanguage, + simulateEditorContentChange, +} from "../__mocks__/monaco-editor"; +import { TextEditor, type TextEditorProps } from "../../src/TextEditor"; + +const defaultProps: TextEditorProps = { + content: "initial content", + language: "json", +}; + +const renderEditor = (props: Partial = {}) => { + let currentProps: TextEditorProps = { ...defaultProps, ...props }; + const result = render(); + + return { + ...result, + rerenderEditor: (nextProps: Partial) => { + currentProps = { ...currentProps, ...nextProps }; + result.rerender(); + }, + }; +}; + +describe("TextEditor", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("package entry point", () => { + it("exports TextEditor from the package index", async () => { + const pkg = await import("../../src/index"); + expect(pkg.TextEditor).toBeDefined(); + }); + }); + + describe("mount", () => { + it("renders a host container that fills its parent", () => { + const { container } = renderEditor(); + const host = container.firstElementChild; + + expect(host).toBeInTheDocument(); + expect(host).toHaveStyle({ width: "100%", height: "100%" }); + }); + + it("creates Monaco once with the initial props", () => { + const { container } = renderEditor({ + content: "hello yaml", + language: "yaml", + isReadOnly: true, + }); + + expect(mockEditorCreate).toHaveBeenCalledOnce(); + expect(mockEditorCreate).toHaveBeenCalledWith( + container.firstElementChild, + expect.objectContaining({ + value: "hello yaml", + language: "yaml", + readOnly: true, + }), + ); + }); + }); + + describe("controlled content", () => { + it("updates Monaco when content changes externally", () => { + const onContentChange = vi.fn(); + const { rerenderEditor } = renderEditor({ content: "initial content", onContentChange }); + + rerenderEditor({ content: "updated content" }); + + expect(mockEditorSetValue).toHaveBeenCalledTimes(1); + expect(mockEditorSetValue).toHaveBeenCalledWith("updated content"); + expect(onContentChange).not.toHaveBeenCalled(); + }); + + it("does not update Monaco when content is unchanged", () => { + const { rerenderEditor } = renderEditor({ content: "same content" }); + + rerenderEditor({ content: "same content" }); + + expect(mockEditorSetValue).not.toHaveBeenCalled(); + }); + + it("calls onContentChange for editor-driven changes", () => { + const onContentChange = vi.fn(); + renderEditor({ onContentChange }); + + simulateEditorContentChange("edited content"); + + expect(onContentChange).toHaveBeenCalledTimes(1); + expect(onContentChange).toHaveBeenCalledWith("edited content"); + }); + }); + + describe("language", () => { + it("updates the model language without recreating Monaco", () => { + const { rerenderEditor } = renderEditor({ language: "json" }); + + rerenderEditor({ language: "yaml" }); + + expect(mockEditorCreate).toHaveBeenCalledTimes(1); + expect(mockSetModelLanguage).toHaveBeenCalledTimes(1); + expect(mockSetModelLanguage).toHaveBeenCalledWith(mockModel, "yaml"); + }); + }); + + describe("read-only", () => { + it("passes readOnly to Monaco at creation time", () => { + const { container } = renderEditor({ isReadOnly: true }); + + expect(mockEditorCreate).toHaveBeenCalledWith( + container.firstElementChild, + expect.objectContaining({ readOnly: true }), + ); + }); + + it("updates readOnly without recreating Monaco", () => { + const { rerenderEditor } = renderEditor({ isReadOnly: false }); + mockEditorUpdateOptions.mockClear(); + + rerenderEditor({ isReadOnly: true }); + + expect(mockEditorCreate).toHaveBeenCalledTimes(1); + expect(mockEditorUpdateOptions).toHaveBeenCalledTimes(1); + expect(mockEditorUpdateOptions).toHaveBeenCalledWith({ readOnly: true }); + }); + }); + + describe("lifecycle", () => { + it("does not recreate Monaco when props change", () => { + const { rerenderEditor } = renderEditor({ content: "v1" }); + + rerenderEditor({ content: "v2" }); + rerenderEditor({ language: "yaml" }); + rerenderEditor({ isReadOnly: true }); + rerenderEditor({ onContentChange: vi.fn() }); + + expect(mockEditorCreate).toHaveBeenCalledTimes(1); + }); + + it("disposes Monaco on unmount", () => { + const { unmount } = renderEditor(); + + unmount(); + + expect(mockEditorDispose).toHaveBeenCalledTimes(1); + }); + }); + + describe("multiple instances", () => { + it("creates one Monaco editor per component instance", () => { + render( + <> + + + , + ); + + expect(mockEditorCreate).toHaveBeenCalledTimes(2); + }); + }); +}); diff --git a/packages/text-editor/tsconfig.json b/packages/text-editor/tsconfig.json new file mode 100644 index 00000000..5dd1032b --- /dev/null +++ b/packages/text-editor/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node", "vite/client"], + "declaration": true, + "declarationMap": true, + "sourceMap": false, + "emitDeclarationOnly": true, + "noEmitOnError": true, + "rootDir": "./src", + "outDir": "./dist", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "dist-storybook"] +} diff --git a/packages/text-editor/tsconfig.test.json b/packages/text-editor/tsconfig.test.json new file mode 100644 index 00000000..81fa7c5a --- /dev/null +++ b/packages/text-editor/tsconfig.test.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "types": ["vitest/globals", "@testing-library/jest-dom/vitest", "node"], + + // Tests can import JS or TS freely + "allowJs": true, + + // Preserve JSX so test runner interprets it + "jsx": "react-jsx", + + // No declaration output during tests + "declaration": false, + "emitDeclarationOnly": false, + "noEmit": true, + + // Tests often work with dynamic/loosely typed objects + "exactOptionalPropertyTypes": false + }, + "include": ["**/*.test.ts", "**/*.test.tsx", "**/tests/**/*"] +} diff --git a/packages/text-editor/vite.config.ts b/packages/text-editor/vite.config.ts new file mode 100644 index 00000000..4dd029a1 --- /dev/null +++ b/packages/text-editor/vite.config.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { defineConfig } from "vite"; + +export default defineConfig({ + resolve: { + tsconfigPaths: true, + }, + build: { + emptyOutDir: false, + sourcemap: true, + lib: { + entry: "src/index.ts", + fileName: (format) => (format === "es" ? "index.js" : `index.${format}.js`), + formats: ["es"], + }, + rollupOptions: { + external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"], + }, + }, +}); diff --git a/packages/text-editor/vitest.config.ts b/packages/text-editor/vitest.config.ts new file mode 100644 index 00000000..f956939e --- /dev/null +++ b/packages/text-editor/vitest.config.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2021-Present The Open Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; +import { storybookTest } from "@storybook/addon-vitest/vitest-plugin"; +import { playwright } from "@vitest/browser-playwright"; + +const dirname = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + resolve: { + tsconfigPaths: true, + }, + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./tests/setupTests.ts"], + projects: [ + { + extends: true, + test: { + name: "unit", + css: true, + include: ["tests/**/*.test.ts", "tests/**/*.test.tsx"], + }, + }, + { + extends: true, + plugins: [ + storybookTest({ + configDir: path.join(dirname, ".storybook"), + }), + ], + test: { + name: "storybook", + browser: { + enabled: true, + headless: true, + provider: playwright({}), + instances: [{ browser: "chromium" }], + }, + }, + }, + ], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8a28b2a1..8d591501 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ catalogs: lucide-react: specifier: ^1.33.0 version: 1.33.0 + monaco-editor: + specifier: 0.56.0 + version: 0.56.0 oxfmt: specifier: ^0.64.0 version: 0.64.0 @@ -395,6 +398,91 @@ importers: specifier: 'catalog:' version: 4.1.11(@types/node@26.2.0)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + packages/text-editor: + dependencies: + monaco-editor: + specifier: 'catalog:' + version: 0.56.0 + devDependencies: + '@chromatic-com/storybook': + specifier: 'catalog:' + version: 5.3.0(storybook@10.5.10(@types/react@19.2.18)(prettier@2.8.8)(react@19.2.8)) + '@playwright/test': + specifier: 'catalog:' + version: 1.62.1 + '@storybook/addon-a11y': + specifier: 'catalog:' + version: 10.5.10(storybook@10.5.10(@types/react@19.2.18)(prettier@2.8.8)(react@19.2.8)) + '@storybook/addon-docs': + specifier: 'catalog:' + version: 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(storybook@10.5.10(@types/react@19.2.18)(prettier@2.8.8)(react@19.2.8))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + '@storybook/addon-vitest': + specifier: 'catalog:' + version: 10.5.10(@vitest/browser-playwright@4.1.11)(@vitest/browser@4.1.11)(@vitest/runner@4.1.11)(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(prettier@2.8.8)(react@19.2.8))(vitest@4.1.11) + '@storybook/react-vite': + specifier: 'catalog:' + version: 10.5.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.10(@types/react@19.2.18)(prettier@2.8.8)(react@19.2.8))(typescript@7.0.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + '@testing-library/dom': + specifier: 'catalog:' + version: 10.4.1 + '@testing-library/jest-dom': + specifier: 'catalog:' + version: 7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.11) + '@testing-library/react': + specifier: 'catalog:' + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@testing-library/user-event': + specifier: 'catalog:' + version: 14.6.6(@testing-library/dom@10.4.1) + '@types/node': + specifier: 'catalog:' + version: 26.2.0 + '@types/react': + specifier: 'catalog:' + version: 19.2.18 + '@types/react-dom': + specifier: 'catalog:' + version: 19.2.5(@types/react@19.2.18) + '@vitest/browser': + specifier: 'catalog:' + version: 4.1.11(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/browser-playwright': + specifier: 'catalog:' + version: 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/coverage-v8': + specifier: 'catalog:' + version: 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11) + '@vitest/ui': + specifier: 'catalog:' + version: 4.1.11(vitest@4.1.11) + jsdom: + specifier: 'catalog:' + version: 30.0.1 + oxfmt: + specifier: 'catalog:' + version: 0.64.0 + oxlint: + specifier: 'catalog:' + version: 1.77.0 + react: + specifier: 'catalog:' + version: 19.2.8 + react-dom: + specifier: 'catalog:' + version: 19.2.8(react@19.2.8) + rimraf: + specifier: 'catalog:' + version: 6.1.3 + storybook: + specifier: 'catalog:' + version: 10.5.10(@types/react@19.2.18)(prettier@2.8.8)(react@19.2.8) + vite: + specifier: 'catalog:' + version: 8.2.2(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0) + vitest: + specifier: 'catalog:' + version: 4.1.11(@types/node@26.2.0)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(jsdom@30.0.1)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)) + packages: '@adobe/css-tools@4.5.0': @@ -2506,6 +2594,9 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@typescript/typescript-aix-ppc64@7.0.2': resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} engines: {node: '>=16.20.0'} @@ -2945,6 +3036,9 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dompurify@3.4.8: + resolution: {integrity: sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==} + electron-to-chromium@1.5.385: resolution: {integrity: sha512-78sa/M08MNAYHQfjoWMvOlKQqZ0ElhSm/L5HNUc96VZ3b+KvDVnngFm8sYQy0XrhTRgAhggHr5abA7yTvRdo4Q==} @@ -3358,6 +3452,11 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + marked@14.0.0: + resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} + engines: {node: '>= 18'} + hasBin: true + mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} @@ -3376,6 +3475,9 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + monaco-editor@0.56.0: + resolution: {integrity: sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -5901,6 +6003,9 @@ snapshots: '@types/resolve@1.20.6': {} + '@types/trusted-types@2.0.7': + optional: true + '@typescript/typescript-aix-ppc64@7.0.2': optional: true @@ -6292,6 +6397,10 @@ snapshots: dom-accessibility-api@0.6.3: {} + dompurify@3.4.8: + optionalDependencies: + '@types/trusted-types': 2.0.7 + electron-to-chromium@1.5.385: {} elkjs@0.12.0: {} @@ -6640,6 +6749,8 @@ snapshots: dependencies: semver: 7.8.5 + marked@14.0.0: {} + mdn-data@2.27.1: {} min-indent@1.0.1: {} @@ -6652,6 +6763,11 @@ snapshots: minipass@7.1.3: {} + monaco-editor@0.56.0: + dependencies: + dompurify: 3.4.8 + marked: 14.0.0 + mrmime@2.0.1: {} ms@2.1.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 06c4a81e..67d604b3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - packages/* - examples/* catalog: + "monaco-editor": "0.56.0" "@changesets/changelog-github": ^1.0.0 "@changesets/cli": ^3.0.1 "@chromatic-com/storybook": ^5.3.0 From 951fbf82790b3caf89bd1f22dd1d499ba210be60 Mon Sep 17 00:00:00 2001 From: fantonangeli Date: Fri, 4 Sep 2026 10:22:33 +0200 Subject: [PATCH 2/7] Reduced monaco bundle size Signed-off-by: fantonangeli --- packages/text-editor/src/TextEditor.tsx | 5 ++++- packages/text-editor/tests-e2e/text-editor.spec.ts | 2 -- packages/text-editor/tests/setupTests.ts | 11 ++++++++++- .../tests/text-editor/TextEditor.story.test.tsx | 5 ----- .../text-editor/tests/text-editor/TextEditor.test.tsx | 5 ----- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/text-editor/src/TextEditor.tsx b/packages/text-editor/src/TextEditor.tsx index f1fa0b81..db1db2c4 100644 --- a/packages/text-editor/src/TextEditor.tsx +++ b/packages/text-editor/src/TextEditor.tsx @@ -15,7 +15,10 @@ */ import * as React from "react"; -import * as monaco from "monaco-editor"; +import * as monaco from "monaco-editor/editor"; +import "monaco-editor/features/register.all"; +import "monaco-editor/languages/features/json/register"; +import "monaco-editor/languages/definitions/yaml/register"; export type TextEditorLanguage = "json" | "yaml"; diff --git a/packages/text-editor/tests-e2e/text-editor.spec.ts b/packages/text-editor/tests-e2e/text-editor.spec.ts index bb4ebcb7..c90d7c1b 100644 --- a/packages/text-editor/tests-e2e/text-editor.spec.ts +++ b/packages/text-editor/tests-e2e/text-editor.spec.ts @@ -19,11 +19,9 @@ import { test, expect } from "@playwright/test"; test("Monaco editor renders and is interactive", async ({ page }) => { await page.goto("/iframe.html?id=text-editor--json-editor"); - // Monaco wraps its content in a div with class .monaco-editor const monacoContainer = page.locator(".monaco-editor").first(); await expect(monacoContainer).toBeVisible(); - // Monaco always renders an internal textarea for keyboard interaction const monacoTextarea = page.locator(".monaco-editor textarea").first(); await expect(monacoTextarea).toBeAttached(); }); diff --git a/packages/text-editor/tests/setupTests.ts b/packages/text-editor/tests/setupTests.ts index 1dd23b59..ff8fd928 100644 --- a/packages/text-editor/tests/setupTests.ts +++ b/packages/text-editor/tests/setupTests.ts @@ -16,6 +16,15 @@ import { cleanup } from "@testing-library/react"; import "@testing-library/jest-dom/vitest"; -import { afterEach } from "vitest"; +import { afterEach, vi } from "vitest"; + +vi.mock("monaco-editor/editor", async () => { + const { default: monacoMock } = await import("./__mocks__/monaco-editor"); + return monacoMock; +}); + +vi.mock("monaco-editor/features/register.all", () => ({})); +vi.mock("monaco-editor/languages/features/json/register", () => ({})); +vi.mock("monaco-editor/languages/definitions/yaml/register", () => ({})); afterEach(cleanup); diff --git a/packages/text-editor/tests/text-editor/TextEditor.story.test.tsx b/packages/text-editor/tests/text-editor/TextEditor.story.test.tsx index f7fcc141..9eba9b5a 100644 --- a/packages/text-editor/tests/text-editor/TextEditor.story.test.tsx +++ b/packages/text-editor/tests/text-editor/TextEditor.story.test.tsx @@ -18,11 +18,6 @@ import { render, screen } from "@testing-library/react"; import { composeStories } from "@storybook/react-vite"; import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("monaco-editor", async () => { - const { default: monacoMock } = await import("../__mocks__/monaco-editor"); - return monacoMock; -}); - import * as stories from "../../stories/features/TextEditor.stories"; import { mockEditorCreate } from "../__mocks__/monaco-editor"; diff --git a/packages/text-editor/tests/text-editor/TextEditor.test.tsx b/packages/text-editor/tests/text-editor/TextEditor.test.tsx index ae3f7606..8883fe24 100644 --- a/packages/text-editor/tests/text-editor/TextEditor.test.tsx +++ b/packages/text-editor/tests/text-editor/TextEditor.test.tsx @@ -18,11 +18,6 @@ import { render } from "@testing-library/react"; import * as React from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("monaco-editor", async () => { - const { default: monacoMock } = await import("../__mocks__/monaco-editor"); - return monacoMock; -}); - import { mockEditorCreate, mockEditorDispose, From 1a7863b27febff320446ee7c021018cdf42a160b Mon Sep 17 00:00:00 2001 From: fantonangeli Date: Fri, 4 Sep 2026 12:08:13 +0200 Subject: [PATCH 3/7] Fixes comment: https://github.com/open-workflow-specification/editor/pull/386#discussion_r3932867403 Signed-off-by: fantonangeli --- packages/text-editor/stories/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/text-editor/stories/helpers.ts b/packages/text-editor/stories/helpers.ts index 4f73df66..bab6aa0b 100644 --- a/packages/text-editor/stories/helpers.ts +++ b/packages/text-editor/stories/helpers.ts @@ -15,7 +15,7 @@ */ import type { StoryObj } from "@storybook/react-vite"; -import type { TextEditor } from "./features/TextEditor"; +import { TextEditor } from "./features/TextEditor"; type Story = StoryObj; From 7ae946378953952094f0c9b014227688a31f9dee Mon Sep 17 00:00:00 2001 From: fantonangeli Date: Fri, 4 Sep 2026 12:12:31 +0200 Subject: [PATCH 4/7] Fixes comment: https://github.com/open-workflow-specification/editor/pull/386#discussion_r3932867460 Signed-off-by: fantonangeli --- packages/text-editor/LICENSE | 201 +++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 packages/text-editor/LICENSE diff --git a/packages/text-editor/LICENSE b/packages/text-editor/LICENSE new file mode 100644 index 00000000..f49a4e16 --- /dev/null +++ b/packages/text-editor/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file From 7add83015a7b5bade7d1c76d0105991636f85f83 Mon Sep 17 00:00:00 2001 From: fantonangeli Date: Fri, 4 Sep 2026 14:24:32 +0200 Subject: [PATCH 5/7] Fixes comment: https://github.com/open-workflow-specification/editor/pull/386#pullrequestreview-5111754566 Signed-off-by: fantonangeli --- packages/text-editor/tests-e2e/text-editor.spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/text-editor/tests-e2e/text-editor.spec.ts b/packages/text-editor/tests-e2e/text-editor.spec.ts index c90d7c1b..2a65a3e1 100644 --- a/packages/text-editor/tests-e2e/text-editor.spec.ts +++ b/packages/text-editor/tests-e2e/text-editor.spec.ts @@ -16,12 +16,15 @@ import { test, expect } from "@playwright/test"; -test("Monaco editor renders and is interactive", async ({ page }) => { +test("Monaco editor is interactive", async ({ page }) => { await page.goto("/iframe.html?id=text-editor--json-editor"); const monacoContainer = page.locator(".monaco-editor").first(); await expect(monacoContainer).toBeVisible(); - const monacoTextarea = page.locator(".monaco-editor textarea").first(); - await expect(monacoTextarea).toBeAttached(); + await monacoContainer.click(); + await page.keyboard.press("Control+A"); + await page.keyboard.type("Lorem ipsum"); + + await expect(monacoContainer).toContainText("Lorem ipsum"); }); From 627d84258592e798ef95f45a9a1010e197a8e40d Mon Sep 17 00:00:00 2001 From: fantonangeli Date: Fri, 4 Sep 2026 14:31:31 +0200 Subject: [PATCH 6/7] Fixes comment: https://github.com/open-workflow-specification/editor/pull/386#discussion_r3933956374 Signed-off-by: fantonangeli --- packages/text-editor/tests-e2e/text-editor.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/text-editor/tests-e2e/text-editor.spec.ts b/packages/text-editor/tests-e2e/text-editor.spec.ts index 2a65a3e1..d5e2606b 100644 --- a/packages/text-editor/tests-e2e/text-editor.spec.ts +++ b/packages/text-editor/tests-e2e/text-editor.spec.ts @@ -23,7 +23,7 @@ test("Monaco editor is interactive", async ({ page }) => { await expect(monacoContainer).toBeVisible(); await monacoContainer.click(); - await page.keyboard.press("Control+A"); + await page.keyboard.press("ControlOrMeta+A"); await page.keyboard.type("Lorem ipsum"); await expect(monacoContainer).toContainText("Lorem ipsum"); From 570c3527110d74cf013f3e7d9f851081c9fc4944 Mon Sep 17 00:00:00 2001 From: fantonangeli Date: Mon, 7 Sep 2026 11:52:11 +0200 Subject: [PATCH 7/7] Fixes comment: https://github.com/open-workflow-specification/editor/pull/386#discussion_r3948140240 Signed-off-by: fantonangeli --- packages/text-editor/vitest.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/text-editor/vitest.config.ts b/packages/text-editor/vitest.config.ts index f956939e..5247ae22 100644 --- a/packages/text-editor/vitest.config.ts +++ b/packages/text-editor/vitest.config.ts @@ -28,14 +28,14 @@ export default defineConfig({ }, test: { globals: true, - environment: "jsdom", - setupFiles: ["./tests/setupTests.ts"], projects: [ { extends: true, test: { name: "unit", css: true, + environment: "jsdom", + setupFiles: ["./tests/setupTests.ts"], include: ["tests/**/*.test.ts", "tests/**/*.test.tsx"], }, },