diff --git a/libs/components/package.json b/libs/components/package.json index 6e1467048d..54444d70df 100644 --- a/libs/components/package.json +++ b/libs/components/package.json @@ -66,6 +66,11 @@ "import": "./circular-progress.mjs", "require": "./circular-progress.js" }, + "./code-editor": { + "types": "./code-editor/code-editor.d.ts", + "import": "./code-editor.mjs", + "require": "./code-editor.js" + }, "./code-snippet": { "types": "./code-snippet/code-snippet.d.ts", "import": "./code-snippet.mjs", @@ -161,6 +166,11 @@ "import": "./menu.mjs", "require": "./menu.js" }, + "./notebook-cell": { + "types": "./notebook-cell/notebook-cell.d.ts", + "import": "./notebook-cell.mjs", + "require": "./notebook-cell.js" + }, "./radio": { "types": "./radio/radio.d.ts", "import": "./radio.mjs", diff --git a/libs/components/project.json b/libs/components/project.json index 9d4aa946f2..e504a83e05 100644 --- a/libs/components/project.json +++ b/libs/components/project.json @@ -15,6 +15,12 @@ "build": { "executor": "nx:run-commands", "options": { + "customWebpackConfig": { + "path": "./monaco-webpack.config.js", + "mergeRules": { + "module.rules": "prepend" + } + }, "commands": [ "mkdir -p dist/libs/components/", "vite build --config libs/components/vite.config.js --outDir dist/libs/components", diff --git a/libs/components/src/code-editor/code-editor.scss b/libs/components/src/code-editor/code-editor.scss new file mode 100644 index 0000000000..fed91a8618 --- /dev/null +++ b/libs/components/src/code-editor/code-editor.scss @@ -0,0 +1,24 @@ +:host { + --cv-editor-width: 100%; + --cv-editor-height: 100%; + + box-sizing: border-box; + + .monaco-editor { + .lines-content.monaco-editor-background { + height: var(--cv-editor-height); + width: var(--cv-editor-width); + } + } + + .monaco-editor-background { + background-color: var(--cv-theme-surface); + } +} + +.editor { + height: var(--cv-editor-height); + min-height: var(--cv-editor-height); + max-width: calc(var(--cv-editor-width) - 2px); + width: var(--cv-editor-width); +} diff --git a/libs/components/src/code-editor/code-editor.spec.ts b/libs/components/src/code-editor/code-editor.spec.ts new file mode 100644 index 0000000000..faf43923ae --- /dev/null +++ b/libs/components/src/code-editor/code-editor.spec.ts @@ -0,0 +1,11 @@ +/** + * @vitest-environment jsdom + */ +import { it, describe, expect } from 'vitest'; +import { CovalentCodeEditor } from './code-editor'; + +describe('Code editor', () => { + it('should work', () => { + expect(new CovalentCodeEditor()).toBeDefined(); + }); +}); diff --git a/libs/components/src/code-editor/code-editor.stories.js b/libs/components/src/code-editor/code-editor.stories.js new file mode 100644 index 0000000000..77d59eb699 --- /dev/null +++ b/libs/components/src/code-editor/code-editor.stories.js @@ -0,0 +1,41 @@ +import './code-editor'; + +const sqlContent = ` +SELECT * FROM load_to_teradata ( + ON ( + SELECT 'class' AS class_col, + 'variable' AS variable_col, + 'type' AS type_col, + category, + cnt, + 'sum' AS sum_col, + 'sumSq', + 'totalCnt' + FROM aster_nb_modelSC + ) + tdpid ('sdt12432.labs.teradata.com') + username ('sample_user') + password ('sample_user') + target_table ('td_nb_modelSC') +); +`; + +export default { + title: 'Components/Code Editor', + args: { + theme: 'cv-light', + code: sqlContent, + language: 'sql', + }, +}; + +const Template = ({ theme, language, code }) => { + return ` +
+ + +
+ `; +}; + +export const Basic = Template.bind(); diff --git a/libs/components/src/code-editor/code-editor.theme.ts b/libs/components/src/code-editor/code-editor.theme.ts new file mode 100644 index 0000000000..4fc921accd --- /dev/null +++ b/libs/components/src/code-editor/code-editor.theme.ts @@ -0,0 +1,94 @@ +import * as tokens from '@covalent/tokens'; + +export const cvEditorDarkTheme = { + base: 'vs-dark', + inherit: true, + rules: [ + { + token: '', + foreground: tokens.CvDarkCodeSnippetColor, + background: tokens.CvThemeDarkColorsSurface, + }, + { + token: 'comment', + foreground: tokens.CvDarkCodeSnippetComment, + fontStyle: 'italic', + }, + { token: 'keyword', foreground: tokens.CvDarkCodeSnippetKeyword }, + { token: 'variable', foreground: tokens.CvDarkCodeSnippetVariable }, + { token: 'string', foreground: tokens.CvDarkCodeSnippetString }, + { token: 'number', foreground: tokens.CvDarkCodeSnippetVariable }, + { token: 'type', foreground: tokens.CvDarkCodeSnippetClass }, + { token: 'class', foreground: tokens.CvDarkCodeSnippetClass }, + { token: 'function', foreground: tokens.CvDarkCodeSnippetTitle }, + { token: 'operator', foreground: tokens.CvDarkCodeSnippetLiteral }, + { token: 'constant', foreground: tokens.CvDarkCodeSnippetLiteral }, + { token: 'builtin', foreground: tokens.CvDarkCodeSnippetClass }, + { token: 'punctuation', foreground: tokens.CvDarkCodeSnippetColor }, + { token: 'meta', foreground: tokens.CvDarkCodeSnippetTitle }, + { token: 'tag', foreground: tokens.CvDarkCodeSnippetSelector }, + { token: 'attribute.name', foreground: tokens.CvDarkCodeSnippetVariable }, + { token: 'attribute.value', foreground: tokens.CvDarkCodeSnippetString }, + { token: 'invalid', foreground: '#f44747' }, + { token: 'strong', fontStyle: 'bold' }, + { token: 'emphasis', fontStyle: 'italic' }, + { + token: 'link', + foreground: tokens.CvDarkCodeSnippetTitle, + fontStyle: 'underline', + }, + ], + colors: { + 'editor.background': tokens.CvThemeDarkColorsSurface, + 'editor.foreground': tokens.CvDarkCodeSnippetColor, + 'editorCursor.foreground': tokens.CvDarkTextSecondaryOnBackground, + }, +}; + +export const cvEditorLightTheme = { + base: 'vs', + inherit: true, + rules: [ + { + token: '', + foreground: tokens.CvLightCodeSnippetColor, + background: tokens.CvLightSurfaceCanvas, + }, + { + token: 'comment', + foreground: tokens.CvLightCodeSnippetComment, + fontStyle: 'italic', + }, + { token: 'keyword', foreground: tokens.CvLightCodeSnippetKeyword }, + { token: 'doctag', foreground: tokens.CvLightCodeSnippetKeyword }, + { token: 'formula', foreground: tokens.CvLightCodeSnippetKeyword }, + { token: 'variable', foreground: tokens.CvLightCodeSnippetVariable }, + { token: 'string', foreground: tokens.CvLightCodeSnippetString }, + { token: 'number', foreground: tokens.CvLightCodeSnippetVariable }, + { token: 'type', foreground: tokens.CvLightCodeSnippetClass }, + { token: 'class', foreground: tokens.CvLightCodeSnippetClass }, + { token: 'function', foreground: tokens.CvLightCodeSnippetTitle }, + { token: 'literal', foreground: tokens.CvLightCodeSnippetLiteral }, + { token: 'operator', foreground: tokens.CvLightCodeSnippetLiteral }, + { token: 'constant', foreground: tokens.CvLightCodeSnippetLiteral }, + { token: 'builtin', foreground: tokens.CvLightCodeSnippetClass }, + { token: 'punctuation', foreground: tokens.CvLightCodeSnippetColor }, + { token: 'meta', foreground: tokens.CvLightCodeSnippetTitle }, + { token: 'tag', foreground: tokens.CvLightCodeSnippetSelector }, + { token: 'attribute.name', foreground: tokens.CvLightCodeSnippetVariable }, + { token: 'attribute.value', foreground: tokens.CvLightCodeSnippetString }, + { token: 'invalid', foreground: '#f44747' }, + { token: 'strong', fontStyle: 'bold' }, + { token: 'emphasis', fontStyle: 'italic' }, + { + token: 'link', + foreground: tokens.CvLightCodeSnippetTitle, + fontStyle: 'underline', + }, + ], + colors: { + 'editor.background': tokens.CvThemeLightColorsSurface, + 'editor.foreground': tokens.CvLightCodeSnippetColor, + 'editorCursor.foreground': tokens.CvLightTextSecondaryOnBackground, + }, +}; diff --git a/libs/components/src/code-editor/code-editor.ts b/libs/components/src/code-editor/code-editor.ts new file mode 100644 index 0000000000..343ddbd340 --- /dev/null +++ b/libs/components/src/code-editor/code-editor.ts @@ -0,0 +1,216 @@ +import { css, html, LitElement, PropertyValues, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { createRef, Ref, ref } from 'lit/directives/ref.js'; + +import styles from './code-editor.scss?inline'; + +// -- Monaco Editor Imports -- +import { editor } from 'monaco-editor/esm/vs/editor/editor.api.js'; +import baseStyles from 'monaco-editor/min/vs/editor/editor.main.css?inline'; + +// Register all language contributions +import 'monaco-editor/esm/vs/basic-languages/html/html.contribution'; +import 'monaco-editor/esm/vs/basic-languages/css/css.contribution'; +import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'; +import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution'; +import 'monaco-editor/esm/vs/basic-languages/python/python.contribution'; +import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution'; +import 'monaco-editor/esm/vs/basic-languages/markdown/markdown.contribution'; +import 'monaco-editor/esm/vs/basic-languages/r/r.contribution'; +import { cvEditorDarkTheme, cvEditorLightTheme } from './code-editor.theme'; + +@customElement('cv-code-editor') +export class CovalentCodeEditor extends LitElement { + /** + * The container where editor will be created + */ + private container: Ref = createRef(); + /** + * Editor instance + */ + editor?: editor.IStandaloneCodeEditor; + /** + * Theme of the editor + */ + @property({ type: String }) theme?: string; + /** + * Language of th editor instance + */ + @property() language?: string; + /** + * Code entered into the editor + */ + @property() code?: string; + /** + * Options that can be set for the editor + */ + @property({ type: Object }) options?: editor.IEditorOptions & + editor.IGlobalEditorOptions; + + static styles = [ + css` + ${unsafeCSS(baseStyles)} ${unsafeCSS(styles)} + `, + ]; + + private getFile() { + if (this.children.length > 0) return this.children[0]; + return null; + } + + private getCode() { + if (this.code) return this.code; + const file = this.getFile(); + if (!file) return; + return file.innerHTML.trim() || ''; + } + + private getLang() { + if (this.language) return this.language; + const file = this.getFile(); + if (!file) return; + const type = file.getAttribute('type'); + return type?.split('/').pop(); + } + + private getTheme() { + if (this.theme) return this.theme; + return 'cv-light'; + } + + private setTheme = () => { + editor.setTheme(this.getTheme()); + }; + + adjustHeight() { + if (this.editor && this.container.value) { + const contentHeight = this.editor.getContentHeight(); + this.container.value.style.height = `${contentHeight}px`; + this.editor.layout(); + } + } + + createEditor(container: HTMLElement) { + // uses covalent light colors + editor.defineTheme('cv-light', { + base: 'vs', + inherit: true, + rules: cvEditorLightTheme.rules, + colors: cvEditorLightTheme.colors, + }); + + // uses covalent dark colors + editor.defineTheme('cv-dark', { + base: 'vs-dark', + inherit: true, + rules: cvEditorDarkTheme.rules, + colors: cvEditorDarkTheme.colors, + }); + + this.editor = editor.create(container, { + ...this.options, + fontLigatures: '', + value: this.getCode(), + language: this.getLang(), + theme: this.getTheme(), + automaticLayout: true, + scrollBeyondLastLine: false, + }); + } + + override disconnectedCallback(): void { + this.editor?.dispose(); + window.removeEventListener('change', this.setTheme); + super.disconnectedCallback(); + } + + firstUpdated() { + if (this.container.value) { + this.createEditor(this.container.value); + + this.onModelChange(); + + // Dispatch event when editor is focused + this.editor?.onDidFocusEditorText(() => { + this.dispatchEvent( + new CustomEvent('editor-focus', { + bubbles: true, + composed: true, + }) + ); + }); + + // Dispatch event when editor is blurred + this.editor?.onDidBlurEditorText(() => { + this.dispatchEvent( + new CustomEvent('editor-blur', { + bubbles: true, + composed: true, + }) + ); + }); + + // Adjust the height of the editor when content changes, to avoid vertical scroll bar + this.editor?.onDidContentSizeChange(() => { + this.adjustHeight(); + }); + + this.adjustHeight(); + + window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', this.setTheme); + } + } + + getValue() { + const value = this.editor?.getValue(); + return value; + } + + onModelChange() { + this.editor?.onDidChangeModelContent(() => { + this.code = this.getValue(); + // Dispatch event when code in the editor is changed + this.dispatchEvent( + new CustomEvent('code-change', { + detail: { code: this.code }, + bubbles: true, + composed: true, + }) + ); + }); + } + + render() { + return html`
`; + } + + setValue(value: string) { + this.editor?.setValue(value); + } + + updated(changedProperties: PropertyValues) { + if (changedProperties.has('code') && this.editor) { + const currentCode = this.editor.getValue(); + if (this.code !== currentCode) { + this.editor.setValue(this.code || ''); + } + } + if (changedProperties.has('language') && this.editor) { + // Update monaco editor language when language prop is changed + editor.setModelLanguage( + this.editor.getModel() as editor.ITextModel, + this.language || '' + ); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + 'cv-code-editor': CovalentCodeEditor; + } +} + +export default CovalentCodeEditor; diff --git a/libs/components/src/code-snippet/code-snippet.ts b/libs/components/src/code-snippet/code-snippet.ts index 92a4c3ad74..eeb2bdc125 100644 --- a/libs/components/src/code-snippet/code-snippet.ts +++ b/libs/components/src/code-snippet/code-snippet.ts @@ -78,7 +78,7 @@ export class CovalentCodeSnippet extends LitElement { return html`
${container}
+ >${container} `; } diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 9665dca50b..70b39aadf7 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -8,6 +8,7 @@ export * from './card/card'; export * from './chips/chip'; export * from './chips/chip-set'; export * from './circular-progress/circular-progress'; +export * from './code-editor/code-editor'; export * from './code-snippet/code-snippet'; export * from './dialog/dialog'; export * from './drawer/drawer'; @@ -27,6 +28,7 @@ export * from './list/list-item'; export * from './list/nav-list-item'; export * from './list/radio-list-item'; export * from './menu/menu'; +export * from './notebook-cell/notebook-cell'; export * from './radio/radio'; export * from './select/select'; export * from './side-sheet/side-sheet'; diff --git a/libs/components/src/notebook-cell/notebook-cell.scss b/libs/components/src/notebook-cell/notebook-cell.scss new file mode 100644 index 0000000000..6f81e748aa --- /dev/null +++ b/libs/components/src/notebook-cell/notebook-cell.scss @@ -0,0 +1,167 @@ +:host { + --mdc-icon-button-size: 24px; + + font-family: var(--mdc-typography-body1-font-family); + width: 100%; +} + +cv-code-snippet { + background-color: var(--cv-theme-surface); + box-sizing: border-box; +} + +cv-code-snippet::part(code) { + font-size: var(--mdc-typography-body2-font-size); + line-height: var(--mdc-typography-body2-line-height); + padding: 0; + white-space: pre-wrap; +} + +cv-code-editor { + margin-top: -1px; +} + +.selectionIndicator { + background-color: var(--cv-theme-primary); + border-radius: 2px; + height: 100%; + visibility: hidden; +} + +.selectionIndicatorWrapper { + min-width: 8px; + + &:hover { + .selectionIndicator { + visibility: visible; + } + } +} + +.status { + padding: 0 1px; + position: relative; +} + +.loading { + top: 2.5px; +} + +.timesExecuted { + align-items: center; + box-sizing: border-box; + color: var(--cv-theme-on-surface-38); + display: flex; + font-family: var(--cv-typography-subtitle2-font-family); + font-size: var(--cv-typography-subtitle2-font-size); + height: 56px; + line-height: var(--cv-typography-headline6-line-height); + + .executionCount { + padding-right: 1rem; + overflow: hidden; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + width: 50px; + } + + cv-icon { + color: var(--cv-theme-on-surface-variant); + padding: 0.5rem; + visibility: hidden; + transition: visibility 0.3s ease; + } +} + +.timesExecutedWrapper { + cursor: grab; + + &:active { + cursor: grabbing; + } + + &:hover { + cv-icon { + visibility: visible; + } + } +} + +.cell { + box-sizing: border-box; + cursor: pointer; + display: flex; + justify-content: space-between; + position: relative; + transition: background-color 0.2s ease; + user-select: auto; + + &.focused, + &.selected { + .selectionIndicator { + visibility: visible; + } + } + + &:hover { + background-color: var(--cv-theme-secondary-4); + + .selectionIndicator { + visibility: visible; + background-color: var(--cv-theme-on-secondary-container-38); + } + } + + &:active { + background-color: var(--cv-theme-secondary-12); + + .selectionIndicator { + visibility: visible; + background-color: var(--cv-theme-on-secondary-container-74); + } + } +} + +.cellWrapper { + display: flex; + flex-direction: column; + width: 100%; +} + +.cellOutputWrapper { + display: flex; + flex-direction: column; + max-width: calc(100% - 95px); + flex: 1; +} + +.contextMenu { + background: white; + border: 1px solid #ccc; + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2); + overflow: auto; + position: absolute; + visibility: hidden; + z-index: 1000; +} + +.contextMenu.open { + visibility: visible; +} + +.editorContainer { + box-sizing: border-box; + background-color: var(--cv-theme-surface); + border: 1px solid var(--cv-theme-outline-variant); + border-radius: var(--cv-editor-border-radius, 4px); + min-height: 56px; + overflow: hidden; + padding: 1rem; +} + +.errors, +.output { + max-width: 100%; + width: 100%; +} diff --git a/libs/components/src/notebook-cell/notebook-cell.spec.ts b/libs/components/src/notebook-cell/notebook-cell.spec.ts new file mode 100644 index 0000000000..975babb06f --- /dev/null +++ b/libs/components/src/notebook-cell/notebook-cell.spec.ts @@ -0,0 +1,11 @@ +/** + * @vitest-environment jsdom + */ +import { it, describe, expect } from 'vitest'; +import { CovalentNotebookCell } from './notebook-cell'; + +describe('Notebook cell', () => { + it('should work', () => { + expect(new CovalentNotebookCell()).toBeDefined(); + }); +}); diff --git a/libs/components/src/notebook-cell/notebook-cell.stories.js b/libs/components/src/notebook-cell/notebook-cell.stories.js new file mode 100644 index 0000000000..be9c4c66c7 --- /dev/null +++ b/libs/components/src/notebook-cell/notebook-cell.stories.js @@ -0,0 +1,68 @@ +import './notebook-cell'; +import '../alert/alert'; +import '../icon/icon'; +import '../typography/typography'; +import '../list/list'; +import '../list/list-item'; + +export default { + title: 'Components/Notebook Cell', + args: { + code: 'Select * from DBC.UserInfo;', + index: 0, + language: 'sql', + loading: false, + selected: true, + hideCount: false, + hideEditor: false, + theme: 'cv-light', + timesExecuted: 2, + }, +}; + +const Template = ({ + code, + index, + language, + loading, + selected, + hideCount, + hideEditor, + theme, + timesExecuted, +}) => { + return `
+ +
+ + +
+
+ Create and Populate Tables + Tables are created and populated using SQL +
+
+ + Cut + Copy + Paste +
Delete
+
  • + Clear outputs + Restart + +
    +
    +
    `; +}; + +export const Basic = Template.bind({}); diff --git a/libs/components/src/notebook-cell/notebook-cell.ts b/libs/components/src/notebook-cell/notebook-cell.ts new file mode 100644 index 0000000000..3546fa88c7 --- /dev/null +++ b/libs/components/src/notebook-cell/notebook-cell.ts @@ -0,0 +1,296 @@ +import { css, html, LitElement, PropertyValues, unsafeCSS } from 'lit'; +import { customElement, property, state } from 'lit/decorators.js'; +import styles from './notebook-cell.scss?inline'; +import { classMap } from 'lit/directives/class-map.js'; + +import '../code-editor/code-editor'; +import '../code-snippet/code-snippet'; +import '../icon-button/icon-button'; +import '../typography/typography'; + +declare global { + interface HTMLElementTagNameMap { + 'cv-notebook-cell': CovalentNotebookCell; + } +} + +enum CvCellEvents { + RUN_CODE = 'cell-run-code', +} + +/** + * Notebook cell + * + * @slot - This element has a slot + */ +@customElement('cv-notebook-cell') +export class CovalentNotebookCell extends LitElement { + /** + * The index of the cell in a notebook + */ + @property({ type: Number }) + index?: number; + + /** + * Code written in the cell + */ + @property({ type: String }) + code = ''; + + /** + * Language of the code + */ + @property({ type: String }) + language = ''; + + /** + * Whether the cell is loading + */ + @property({ type: Boolean, reflect: true }) + loading = false; + + /** + * Whether the cell is selected + */ + @property({ type: Boolean, reflect: true }) + selected = false; + + /** + * Number of times the cell was exceuted + */ + @property({ type: Number }) + timesExecuted = 0; + + /** + * Whether the editor is shown + */ + @property({ type: Boolean, reflect: true }) + hideEditor = false; + + /** + * Whether the execution count is shown + */ + @property({ type: Boolean, reflect: true }) + hideCount = false; + + /** + * Whether the execution count is shown + */ + @property({ type: String }) + editorTheme = ''; + + private _editorFocused = false; + + @state() + private _isMenuOpen = false; + + private _menuMaxHeight = 'auto'; + + private _menuPosition = { top: 0, left: 0 }; + + editorOptions = { + minimap: { enabled: false }, // Disable minimap to save space + wordWrap: 'on', // Enable word wrap to avoid horizontal scroll + fontSize: '14px', + glyphMargin: false, + folding: false, + lineHeight: 20, + lineNumbers: 'off', + lineDecorationsWidth: 0, + lineNumbersMinChars: 0, + renderLineHighlight: 'none', + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + scrollbar: { + alwaysConsumeMouseWheel: false, + vertical: 'hidden', + }, + }; + + static override styles = [ + css` + ${unsafeCSS(styles)} + `, + ]; + + constructor() { + super(); + this.closeContextMenu = this.closeContextMenu.bind(this); + this.showContextMenu = this.showContextMenu.bind(this); + } + + closeContextMenu(): void { + this._isMenuOpen = false; + } + + connectedCallback(): void { + super.connectedCallback(); + document.addEventListener('click', this.closeContextMenu); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + document.removeEventListener('click', this.closeContextMenu); + } + + handleCodeChange(e: CustomEvent) { + this.code = e.detail.code; + } + + handleRun() { + this.dispatchEvent( + new CustomEvent(CvCellEvents.RUN_CODE, { + detail: { index: this.index, code: this.code }, + bubbles: true, + composed: true, + }) + ); + } + + setEditorFocus(setFocus: boolean) { + this._editorFocused = setFocus; + this.requestUpdate(); + } + + showContextMenu(e: MouseEvent) { + e.preventDefault(); + const cells = document.querySelectorAll('cv-notebook-cell'); + cells?.forEach((cell) => { + cell.closeContextMenu(); + }); + + const menu = this.shadowRoot?.querySelector( + '.contextMenu' + ) as HTMLDivElement; + const menuHeight = menu?.offsetHeight; + + // Determine if there is enough space below the click position + const viewportHeight = window.innerHeight; + const spaceBelow = viewportHeight - e.clientY; + const spaceAbove = e.clientY; + + // Determine if there is enough space below, otherwise open upwards + if (spaceBelow >= menuHeight) { + // Enough space below, open normally + this._menuPosition = { top: e.clientY, left: e.clientX }; + this._menuMaxHeight = `${spaceBelow}px`; + } else if (spaceAbove >= menuHeight) { + // Enough space above, open upwards fully + this._menuPosition = { top: e.clientY - menuHeight, left: e.clientX }; + this._menuMaxHeight = `${menuHeight}px`; + } else { + // Not enough space either way, open upwards with adjusted height + this._menuPosition = { top: e.clientY - spaceAbove, left: e.clientX }; + this._menuMaxHeight = `${spaceAbove}px`; + } + this._isMenuOpen = true; + } + + protected updated(changedProperties: PropertyValues) { + const editor = this.shadowRoot?.querySelector('cv-code-editor'); + if (changedProperties.has('code') || changedProperties.has('language')) { + // Update the editor instance's code and language properties + if (editor) { + editor.code = this.code; + editor.language = this.language; + } else { + // Update the code in code-snippet component + const codeSnippet = this.shadowRoot?.querySelector('cv-code-snippet'); + if (codeSnippet && !this.selected) { + codeSnippet.highlight(); + } + } + } + super.updated(changedProperties); + } + + getEditorBgColor(): string { + const editorContainer = this.shadowRoot?.querySelector('.editorContainer'); + if (!editorContainer) { + return ''; + } + const styles = window.getComputedStyle(editorContainer); + return styles.getPropertyValue('background-color'); + } + + renderEditor() { + // Show editor when the cell is selected and show code snippet otherwise + const editor = this.selected + ? html`` + : html`${this.code}`; + return html`${this.hideEditor + ? '' + : html`
    ${editor}
    `}`; + } + + renderOutput() { + const output = html`
    +
    + + +
    `; + return html`${output}`; + } + + renderExecutionCount() { + if (this.hideCount) { + return html` `; + } + const loadingClass = { + status: true, + loading: this.loading, + }; + return html`[${this.loading ? '*' : this.timesExecuted || ' '}] :`; + } + + protected render() { + const classes = { + cell: true, + selected: this.selected, + focused: this._editorFocused, + }; + return html` +
    +
    +
    +
    + +
    +
    + drag_indicator +
    ${this.renderExecutionCount()}
    +
    +
    + +
    + ${this.renderEditor()} ${this.renderOutput()} +
    +
    +
    + +
    + `; + } +} + +export default CovalentNotebookCell; diff --git a/libs/components/src/select/select.scss b/libs/components/src/select/select.scss index 5f6f5b4e20..82af225eaa 100644 --- a/libs/components/src/select/select.scss +++ b/libs/components/src/select/select.scss @@ -5,10 +5,29 @@ --mdc-select-ink-color: var(--mdc-theme-text-primary-on-background); --mdc-select-label-ink-color: var(--mdc-theme-text-secondary-on-background); --mdc-select-outlined-idle-border-color: var(--mdc-theme-border); - --mdc-select-outlined-hover-border-color: var(--mdc-theme-text-icon-on-background); + --mdc-select-outlined-hover-border-color: var( + --mdc-theme-text-icon-on-background + ); --mdc-select-dropdown-icon-color: var(--mdc-theme-text-icon-on-background); .mdc-select:not(.mdc-select--disabled) .mdc-select__icon { - color: var(--mdc-select-dropdown-icon-color) + color: var(--mdc-select-dropdown-icon-color); + } + + // provides consumer an option to set height + .mdc-select--outlined .mdc-select__anchor, + .mdc-select--filled .mdc-select__anchor { + height: var(--cv-select-height, 56px); + } + + // provides consumer an option to adjust the position of label in the outlined version + .mdc-select--outlined:not(.mdc-select--with-leading-icon) + .mdc-select__anchor + .mdc-floating-label--float-above, + .mdc-select--filled:not(.mdc-select--with-leading-icon) + .mdc-select__anchor + .mdc-floating-label--float-above { + transform: translateY(var(--cv-select-label-top-position, -37.25px)) + scale(1); } } diff --git a/libs/components/vite.config.js b/libs/components/vite.config.js index e446ebd210..c839388730 100644 --- a/libs/components/vite.config.js +++ b/libs/components/vite.config.js @@ -20,6 +20,7 @@ module.exports = defineConfig(({ mode }) => { 'libs/components/src/chips/chip', 'libs/components/src/chips/chip-set', 'libs/components/src/circular-progress/circular-progress', + 'libs/components/src/code-editor/code-editor', 'libs/components/src/code-snippet/code-snippet', 'libs/components/src/dialog/dialog', 'libs/components/src/drawer/drawer', @@ -39,6 +40,7 @@ module.exports = defineConfig(({ mode }) => { 'libs/components/src/list/nav-list-item', 'libs/components/src/list/radio-list-item', 'libs/components/src/menu/menu', + 'libs/components/src/notebook-cell/notebook-cell', 'libs/components/src/radio/radio', 'libs/components/src/select/select', 'libs/components/src/side-sheet/side-sheet', @@ -63,6 +65,9 @@ module.exports = defineConfig(({ mode }) => { 'libs/components/src/typography/typography', ], name: 'Covalent', + rollupOptions: { + external: ['monaco-editor'], + }, }, }, server: {