From 2787f32bcaf1e7dd8ca272b70c1b430c309b71b4 Mon Sep 17 00:00:00 2001 From: Zack Moore Date: Fri, 17 Jan 2025 09:53:25 -0500 Subject: [PATCH] HDS::CodeEditor with Code Mirror 6 (#2573) Co-authored-by: Alex Co-authored-by: Kristin Bradley --- .changeset/long-poets-wonder.md | 6 + packages/components/babel.config.json | 3 +- packages/components/package.json | 20 + packages/components/src/components.ts | 6 + .../hds/code-editor/description.hbs | 3 + .../components/hds/code-editor/description.ts | 20 + .../hds/code-editor/full-screen-button.hbs | 11 + .../hds/code-editor/full-screen-button.ts | 35 ++ .../components/hds/code-editor/generic.hbs | 7 + .../src/components/hds/code-editor/generic.ts | 18 + .../src/components/hds/code-editor/index.hbs | 70 +++ .../src/components/hds/code-editor/index.ts | 136 ++++++ .../src/components/hds/code-editor/title.hbs | 11 + .../src/components/hds/code-editor/title.ts | 30 ++ .../src/modifiers/hds-code-editor.ts | 345 ++++++++++++++ .../hds-dark-highlight-style.ts | 49 ++ .../palettes/hds-dark-palette.ts | 18 + .../hds-code-editor/themes/hds-dark-theme.ts | 72 +++ .../src/modifiers/hds-code-editor/types.ts | 16 + .../@hashicorp/design-system-components.scss | 1 + .../styles/components/code-editor/index.scss | 113 +++++ .../styles/components/code-editor/theme.scss | 20 + packages/components/src/template-registry.ts | 21 + .../app/controllers/components/code-editor.js | 94 ++++ showcase/app/router.ts | 1 + showcase/app/routes/components/code-editor.js | 8 + showcase/app/styles/app.scss | 1 + .../styles/showcase-pages/code-editor.scss | 12 + .../app/templates/components/code-editor.hbs | 137 ++++++ showcase/app/templates/index.hbs | 5 + .../acceptance/components/hds/code-editor.js | 21 + showcase/tests/acceptance/percy-test.js | 6 +- .../code-editor/full-screen-button-test.js | 76 ++++ .../components/hds/code-editor/index-test.js | 257 +++++++++++ .../components/hds/code-editor/title-test.js | 60 +++ .../modifiers/hds-code-editor-test.js | 114 +++++ yarn.lock | 421 +++++++++++++++--- 37 files changed, 2191 insertions(+), 53 deletions(-) create mode 100644 .changeset/long-poets-wonder.md create mode 100644 packages/components/src/components/hds/code-editor/description.hbs create mode 100644 packages/components/src/components/hds/code-editor/description.ts create mode 100644 packages/components/src/components/hds/code-editor/full-screen-button.hbs create mode 100644 packages/components/src/components/hds/code-editor/full-screen-button.ts create mode 100644 packages/components/src/components/hds/code-editor/generic.hbs create mode 100644 packages/components/src/components/hds/code-editor/generic.ts create mode 100644 packages/components/src/components/hds/code-editor/index.hbs create mode 100644 packages/components/src/components/hds/code-editor/index.ts create mode 100644 packages/components/src/components/hds/code-editor/title.hbs create mode 100644 packages/components/src/components/hds/code-editor/title.ts create mode 100644 packages/components/src/modifiers/hds-code-editor.ts create mode 100644 packages/components/src/modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.ts create mode 100644 packages/components/src/modifiers/hds-code-editor/palettes/hds-dark-palette.ts create mode 100644 packages/components/src/modifiers/hds-code-editor/themes/hds-dark-theme.ts create mode 100644 packages/components/src/modifiers/hds-code-editor/types.ts create mode 100644 packages/components/src/styles/components/code-editor/index.scss create mode 100644 packages/components/src/styles/components/code-editor/theme.scss create mode 100644 showcase/app/controllers/components/code-editor.js create mode 100644 showcase/app/routes/components/code-editor.js create mode 100644 showcase/app/styles/showcase-pages/code-editor.scss create mode 100644 showcase/app/templates/components/code-editor.hbs create mode 100644 showcase/tests/acceptance/components/hds/code-editor.js create mode 100644 showcase/tests/integration/components/hds/code-editor/full-screen-button-test.js create mode 100644 showcase/tests/integration/components/hds/code-editor/index-test.js create mode 100644 showcase/tests/integration/components/hds/code-editor/title-test.js create mode 100644 showcase/tests/integration/modifiers/hds-code-editor-test.js diff --git a/.changeset/long-poets-wonder.md b/.changeset/long-poets-wonder.md new file mode 100644 index 00000000000..f0d1ddb92f9 --- /dev/null +++ b/.changeset/long-poets-wonder.md @@ -0,0 +1,6 @@ +--- +"@hashicorp/design-system-components": minor +--- + +`Hds::CodeEditor` - Added new CodeMirror 6 supported code editor component +`hds-code-editor` modifier - Added new code editor modifier which converts the element it is applied to into a CodeMirror 6 code editor \ No newline at end of file diff --git a/packages/components/babel.config.json b/packages/components/babel.config.json index 388b999f4bb..fe5e1d78209 100644 --- a/packages/components/babel.config.json +++ b/packages/components/babel.config.json @@ -26,6 +26,7 @@ ], ["@babel/plugin-proposal-decorators", { "legacy": true }], "@babel/plugin-transform-class-properties", - "@babel/plugin-transform-private-methods" + "@babel/plugin-transform-private-methods", + "ember-concurrency/async-arrow-task-transform" ] } diff --git a/packages/components/package.json b/packages/components/package.json index 3001173645f..9e1dd47a84d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -35,6 +35,15 @@ "lint:js:fix": "eslint . --fix" }, "dependencies": { + "@codemirror/commands": "^6.8.0", + "@codemirror/lang-go": "^6.0.1", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-sql": "^6.8.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/language": "^6.10.3", + "@codemirror/legacy-modes": "^6.4.2", + "@codemirror/state": "^6.5.0", + "@codemirror/view": "^6.36.2", "@ember/render-modifiers": "^2.1.0", "@ember/string": "^3.1.1", "@ember/test-waiters": "^3.1.0", @@ -43,6 +52,7 @@ "@hashicorp/design-system-tokens": "^2.2.2", "@hashicorp/flight-icons": "^3.8.0", "clipboard-polyfill": "^4.1.1", + "codemirror-lang-hcl": "^0.0.0-beta.2", "decorator-transforms": "^1.2.1", "ember-a11y-refocus": "^4.1.4", "ember-cli-sass": "^11.0.1", @@ -155,6 +165,11 @@ "./components/hds/code-block/description.js": "./dist/_app_/components/hds/code-block/description.js", "./components/hds/code-block/index.js": "./dist/_app_/components/hds/code-block/index.js", "./components/hds/code-block/title.js": "./dist/_app_/components/hds/code-block/title.js", + "./components/hds/code-editor/description.js": "./dist/_app_/components/hds/code-editor/description.js", + "./components/hds/code-editor/full-screen-button.js": "./dist/_app_/components/hds/code-editor/full-screen-button.js", + "./components/hds/code-editor/generic.js": "./dist/_app_/components/hds/code-editor/generic.js", + "./components/hds/code-editor/index.js": "./dist/_app_/components/hds/code-editor/index.js", + "./components/hds/code-editor/title.js": "./dist/_app_/components/hds/code-editor/title.js", "./components/hds/copy/button/index.js": "./dist/_app_/components/hds/copy/button/index.js", "./components/hds/copy/snippet/index.js": "./dist/_app_/components/hds/copy/snippet/index.js", "./components/hds/dialog-primitive/body.js": "./dist/_app_/components/hds/dialog-primitive/body.js", @@ -300,6 +315,11 @@ "./instance-initializers/load-sprite.js": "./dist/_app_/instance-initializers/load-sprite.js", "./modifiers/hds-anchored-position.js": "./dist/_app_/modifiers/hds-anchored-position.js", "./modifiers/hds-clipboard.js": "./dist/_app_/modifiers/hds-clipboard.js", + "./modifiers/hds-code-editor.js": "./dist/_app_/modifiers/hds-code-editor.js", + "./modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.js": "./dist/_app_/modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.js", + "./modifiers/hds-code-editor/palettes/hds-dark-palette.js": "./dist/_app_/modifiers/hds-code-editor/palettes/hds-dark-palette.js", + "./modifiers/hds-code-editor/themes/hds-dark-theme.js": "./dist/_app_/modifiers/hds-code-editor/themes/hds-dark-theme.js", + "./modifiers/hds-code-editor/types.js": "./dist/_app_/modifiers/hds-code-editor/types.js", "./modifiers/hds-register-event.js": "./dist/_app_/modifiers/hds-register-event.js", "./modifiers/hds-tooltip.js": "./dist/_app_/modifiers/hds-tooltip.js", "./services/hds-time.js": "./dist/_app_/services/hds-time.js" diff --git a/packages/components/src/components.ts b/packages/components/src/components.ts index a377e8f6830..a05db1eea3d 100644 --- a/packages/components/src/components.ts +++ b/packages/components/src/components.ts @@ -72,6 +72,12 @@ export { default as HdsCodeBlockDescription } from './components/hds/code-block/ export { default as HdsCodeBlockTitle } from './components/hds/code-block/title.ts'; export * from './components/hds/code-block/types.ts'; +// CodeEditor +export { default as HdsCodeEditor } from './components/hds/code-editor/index.ts'; +export { default as HdsCodeEditorDescription } from './components/hds/code-editor/description.ts'; +export { default as HdsCodeEditorTitle } from './components/hds/code-editor/title.ts'; +export { default as HdsCodeEditorFullScreenButton } from './components/hds/code-editor/full-screen-button.ts'; + // CopyButton export { default as HdsCopyButton } from './components/hds/copy/button/index.ts'; export * from './components/hds/copy/button/types.ts'; diff --git a/packages/components/src/components/hds/code-editor/description.hbs b/packages/components/src/components/hds/code-editor/description.hbs new file mode 100644 index 00000000000..0e979e6ed25 --- /dev/null +++ b/packages/components/src/components/hds/code-editor/description.hbs @@ -0,0 +1,3 @@ + + {{yield}} + \ No newline at end of file diff --git a/packages/components/src/components/hds/code-editor/description.ts b/packages/components/src/components/hds/code-editor/description.ts new file mode 100644 index 00000000000..dfd404204a8 --- /dev/null +++ b/packages/components/src/components/hds/code-editor/description.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import TemplateOnlyComponent from '@ember/component/template-only'; + +import type { HdsTextBodySignature } from '../text/body'; + +export interface HdsCodeEditorDescriptionSignature { + Blocks: { + default: []; + }; + Element: HdsTextBodySignature['Element']; +} + +const HdsCodeEditorDescription = + TemplateOnlyComponent(); + +export default HdsCodeEditorDescription; diff --git a/packages/components/src/components/hds/code-editor/full-screen-button.hbs b/packages/components/src/components/hds/code-editor/full-screen-button.hbs new file mode 100644 index 00000000000..d3de4d5f020 --- /dev/null +++ b/packages/components/src/components/hds/code-editor/full-screen-button.hbs @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/packages/components/src/components/hds/code-editor/full-screen-button.ts b/packages/components/src/components/hds/code-editor/full-screen-button.ts new file mode 100644 index 00000000000..b9816966067 --- /dev/null +++ b/packages/components/src/components/hds/code-editor/full-screen-button.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; + +import type { HdsButtonSignature } from '../button'; + +export interface HdsCodeEditorFullScreenButtonSignature { + Args: { + isFullScreen: boolean; + onToggleFullScreen: () => void; + }; + Element: HdsButtonSignature['Element']; +} + +export default class HdsCodeEditorFullScreenButton extends Component { + get state(): 'minimize' | 'maximize' { + return this.args.isFullScreen ? 'minimize' : 'maximize'; + } + + get className(): string { + const classes = [ + 'hds-code-editor__full-screen-button', + 'hds-code-editor__button', + ]; + + const stateClass = `hds-code-editor__full-screen-button--${this.state}`; + + classes.push(stateClass); + + return classes.join(' '); + } +} diff --git a/packages/components/src/components/hds/code-editor/generic.hbs b/packages/components/src/components/hds/code-editor/generic.hbs new file mode 100644 index 00000000000..18e31e012d8 --- /dev/null +++ b/packages/components/src/components/hds/code-editor/generic.hbs @@ -0,0 +1,7 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} +
+ {{yield}} +
\ No newline at end of file diff --git a/packages/components/src/components/hds/code-editor/generic.ts b/packages/components/src/components/hds/code-editor/generic.ts new file mode 100644 index 00000000000..354252a3721 --- /dev/null +++ b/packages/components/src/components/hds/code-editor/generic.ts @@ -0,0 +1,18 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import TemplateOnlyComponent from '@ember/component/template-only'; + +export interface HdsCodeEditorGenericSignature { + Blocks: { + default: []; + }; + Element: HTMLDivElement; +} + +const HdsCodeEditorGeneric = + TemplateOnlyComponent(); + +export default HdsCodeEditorGeneric; diff --git a/packages/components/src/components/hds/code-editor/index.hbs b/packages/components/src/components/hds/code-editor/index.hbs new file mode 100644 index 00000000000..be1163bbb2e --- /dev/null +++ b/packages/components/src/components/hds/code-editor/index.hbs @@ -0,0 +1,70 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + +
+ {{! header }} + {{#if (or this.hasActions (has-block))}} +
+
+ {{yield + (hash + Title=(component "hds/code-editor/title" editorId=this._id onInsert=this.registerTitleElement) + Description=(component "hds/code-editor/description") + Generic=(component "hds/code-editor/generic") + ) + }} +
+ + {{#if this.hasActions}} +
+ {{#if @hasCopyButton}} + + {{/if}} + {{#if @hasFullScreenButton}} + + {{/if}} +
+ {{/if}} +
+ {{/if}} + + {{! editor }} +
+ + {{! loader }} + {{#unless this._isSetupComplete}} +
+ + Loading +
+ {{/unless}} +
\ No newline at end of file diff --git a/packages/components/src/components/hds/code-editor/index.ts b/packages/components/src/components/hds/code-editor/index.ts new file mode 100644 index 00000000000..4a2130876b7 --- /dev/null +++ b/packages/components/src/components/hds/code-editor/index.ts @@ -0,0 +1,136 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { modifier } from 'ember-modifier'; + +import type { ComponentLike } from '@glint/template'; +import type { HdsCodeEditorSignature as HdsCodeEditorModifierSignature } from 'src/modifiers/hds-code-editor'; +import type { HdsCodeEditorDescriptionSignature } from './description'; +import type { HdsCodeEditorTitleSignature } from './title'; +import type { HdsCodeEditorGenericSignature } from './generic'; +import type { EditorView } from '@codemirror/view'; +import { guidFor } from '@ember/object/internals'; + +export interface HdsCodeEditorSignature { + Args: { + ariaLabel?: string; + ariaLabelledBy?: string; + hasCopyButton?: boolean; + hasFullScreenButton?: boolean; + isStandalone?: boolean; + language?: HdsCodeEditorModifierSignature['Args']['Named']['language']; + value?: HdsCodeEditorModifierSignature['Args']['Named']['value']; + onBlur?: HdsCodeEditorModifierSignature['Args']['Named']['onBlur']; + onInput?: HdsCodeEditorModifierSignature['Args']['Named']['onInput']; + onSetup?: HdsCodeEditorModifierSignature['Args']['Named']['onSetup']; + }; + Blocks: { + default: [ + { + Title?: ComponentLike; + Description?: ComponentLike; + Generic?: ComponentLike; + }, + ]; + }; + Element: HTMLDivElement; +} + +export default class HdsCodeEditor extends Component { + @tracked private _isFullScreen = false; + @tracked private _isSetupComplete = false; + @tracked private _value; + @tracked private _titleId: string | undefined; + + private _id = guidFor(this); + + private _handleEscape = modifier(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Escape' || !this._isFullScreen) { + return; + } + + this.toggleFullScreen(); + }; + + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }); + + constructor(owner: unknown, args: HdsCodeEditorSignature['Args']) { + super(owner, args); + + if (args.value) { + this._value = args.value; + } + } + + get ariaLabelledBy(): string | undefined { + if (this.args.ariaLabel !== undefined) { + return; + } + + return this.args.ariaLabelledBy ?? this._titleId; + } + + get hasActions(): boolean { + return (this.args.hasCopyButton || this.args.hasFullScreenButton) ?? false; + } + + get isStandalone(): boolean { + return this.args.isStandalone ?? true; + } + + get classNames(): string { + // Currently there is only one theme so the class name is hard-coded. + // In the future, additional themes such as a "light" theme could be added. + const classes = ['hds-code-editor', 'hds-code-editor--theme-dark']; + + if (this._isFullScreen) { + classes.push('hds-code-editor--is-full-screen'); + } + + if (this.isStandalone) { + classes.push('hds-code-editor--is-standalone'); + } + + return classes.join(' '); + } + + @action + registerTitleElement(element: HdsCodeEditorTitleSignature['Element']): void { + this._titleId = element.id; + } + + @action + toggleFullScreen(): void { + this._isFullScreen = !this._isFullScreen; + } + + @action + onInput(newValue: string): void { + this._value = newValue; + this.args.onInput?.(newValue); + } + + @action + onKeyDown(event: KeyboardEvent): void { + if (event.key === 'Escape' && this._isFullScreen) { + this.toggleFullScreen(); + } + } + + @action + onSetup(editorView: EditorView): void { + this._isSetupComplete = true; + this.args.onSetup?.(editorView); + } +} diff --git a/packages/components/src/components/hds/code-editor/title.hbs b/packages/components/src/components/hds/code-editor/title.hbs new file mode 100644 index 00000000000..aaa8b6c5679 --- /dev/null +++ b/packages/components/src/components/hds/code-editor/title.hbs @@ -0,0 +1,11 @@ + + {{yield}} + \ No newline at end of file diff --git a/packages/components/src/components/hds/code-editor/title.ts b/packages/components/src/components/hds/code-editor/title.ts new file mode 100644 index 00000000000..032a6ef5120 --- /dev/null +++ b/packages/components/src/components/hds/code-editor/title.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Component from '@glimmer/component'; + +import type { HdsTextBodySignature } from '../text/body'; + +type HdsCodeEditorTitleElement = HdsTextBodySignature['Element']; + +export interface HdsCodeEditorTitleSignature { + Args: { + editorId: string; + tag?: HdsTextBodySignature['Args']['tag']; + onInsert: (element: HdsCodeEditorTitleElement) => void; + }; + Blocks: { + default: []; + }; + Element: HdsCodeEditorTitleElement; +} + +export default class HdsCodeEditorTitle extends Component { + private _id = `${this.args.editorId}-title`; + + get tag() { + return this.args.tag ?? 'h2'; + } +} diff --git a/packages/components/src/modifiers/hds-code-editor.ts b/packages/components/src/modifiers/hds-code-editor.ts new file mode 100644 index 00000000000..9ad9d98a16a --- /dev/null +++ b/packages/components/src/modifiers/hds-code-editor.ts @@ -0,0 +1,345 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Modifier from 'ember-modifier'; +import { assert, warn } from '@ember/debug'; +import { registerDestructor } from '@ember/destroyable'; +import { task } from 'ember-concurrency'; +import config from 'ember-get-config'; + +// hds-dark theme +import hdsDarkTheme from './hds-code-editor/themes/hds-dark-theme.ts'; +import hdsDarkHighlightStyle from './hds-code-editor/highlight-styles/hds-dark-highlight-style.ts'; + +import type { HdsCodeEditorLanguages } from './hds-code-editor/types.ts'; +import type { ArgsFor, PositionalArgs, NamedArgs } from 'ember-modifier'; +import type { + StreamLanguage as StreamLanguageType, + StreamParser as StreamParserType, +} from '@codemirror/language'; +import type { Extension } from '@codemirror/state'; +import type { EditorView, ViewUpdate } from '@codemirror/view'; + +type HdsCodeEditorBlurHandler = (editor: EditorView, event: FocusEvent) => void; + +export interface HdsCodeEditorSignature { + Args: { + Named: { + ariaLabel?: string; + ariaLabelledBy?: string; + language?: HdsCodeEditorLanguages; + value?: string; + onInput?: (newVal: string) => void; + onBlur?: HdsCodeEditorBlurHandler; + onSetup?: (editor: EditorView) => unknown; + }; + }; +} + +async function defineStreamLanguage(streamParser: StreamParserType) { + const { StreamLanguage } = await import('@codemirror/language'); + + return StreamLanguage.define(streamParser); +} + +const LOADER_HEIGHT = '164px'; + +const LANGUAGES: Record< + HdsCodeEditorLanguages, + { load: () => Promise> } +> = { + ruby: { + load: async () => { + const { ruby } = await import('@codemirror/legacy-modes/mode/ruby'); + return defineStreamLanguage(ruby); + }, + }, + shell: { + load: async () => { + const { shell } = await import('@codemirror/legacy-modes/mode/shell'); + return defineStreamLanguage(shell); + }, + }, + go: { + load: async () => (await import('@codemirror/lang-go')).go(), + }, + hcl: { + load: async () => (await import('codemirror-lang-hcl')).hcl(), + }, + json: { + load: async () => (await import('@codemirror/lang-json')).json(), + }, + sql: { + load: async () => (await import('@codemirror/lang-sql')).sql(), + }, + yaml: { + load: async () => (await import('@codemirror/lang-yaml')).yaml(), + }, +} as const; + +export default class HdsCodeEditorModifier extends Modifier { + editor!: EditorView; + element!: HTMLElement; + + onBlur: HdsCodeEditorSignature['Args']['Named']['onBlur']; + onInput: HdsCodeEditorSignature['Args']['Named']['onInput']; + + blurHandler!: (event: FocusEvent) => void; + observer!: IntersectionObserver; + + constructor( + owner: HdsCodeEditorModifier, + args: ArgsFor + ) { + super(owner, args); + + registerDestructor(this, () => { + this.observer?.disconnect(); + + if (this.onBlur !== undefined) { + this.element.removeEventListener('blur', this.blurHandler); + } + }); + } + + modify( + element: HTMLElement, + positional: PositionalArgs, + named: NamedArgs + ): void { + // the intersection observer makes loading unreliable in tests + if (config.environment === 'test') { + this._setupTask.perform(element, positional, named); + } else { + this.observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + const setupHasNotRun = this._setupTask.performCount === 0; + + if (entry.isIntersecting && setupHasNotRun) { + this._setupTask.perform(element, positional, named); + } + }); + }, + { + rootMargin: LOADER_HEIGHT, + } + ); + + this.observer.observe(element); + } + } + + private _setupEditorBlurHandler( + element: HTMLElement, + onBlur: HdsCodeEditorBlurHandler + ) { + const inputElement = element.querySelector('.cm-content'); + + if (inputElement === null) { + return; + } + + this.blurHandler = (event: FocusEvent) => onBlur(this.editor, event); + + (inputElement as HTMLElement).addEventListener('blur', this.blurHandler); + } + + private _setupEditorAriaAttributes( + editor: EditorView, + { + ariaLabel, + ariaLabelledBy, + }: Pick< + HdsCodeEditorSignature['Args']['Named'], + 'ariaLabel' | 'ariaLabelledBy' + > + ) { + assert( + '`hds-code-editor` modifier - Either `ariaLabel` or `ariaLabelledBy` must be provided', + ariaLabel !== undefined || ariaLabelledBy !== undefined + ); + + if (ariaLabel !== undefined) { + editor.dom + .querySelector('[role="textbox"]') + ?.setAttribute('aria-label', ariaLabel); + } else if (ariaLabelledBy !== undefined) { + editor.dom + .querySelector('[role="textbox"]') + ?.setAttribute('aria-labelledby', ariaLabelledBy); + } + } + + private _loadLanguageTask = task( + { drop: true }, + async (language?: HdsCodeEditorLanguages) => { + if (language === undefined) { + return; + } + + try { + const validLanguageKeys = Object.keys(LANGUAGES); + + assert( + `\`hds-code-editor\` modifier - \`language\` must be one of the following: ${validLanguageKeys.join( + ', ' + )}; received: ${language}`, + validLanguageKeys.includes(language) + ); + + return LANGUAGES[language].load(); + } catch (error) { + warn( + `\`hds-code-editor\` modifier - Failed to dynamically import the CodeMirror language module for '${language}'. Error: ${JSON.stringify( + error + )}`, + { + id: 'hds-code-editor.load-language-task.import-failed', + } + ); + } + } + ); + + private _buildExtensionsTask = task({ drop: true }, async ({ language }) => { + const [ + { + EditorView, + keymap, + lineNumbers, + highlightActiveLineGutter, + highlightSpecialChars, + highlightActiveLine, + }, + { defaultKeymap, history, historyKeymap }, + { bracketMatching, syntaxHighlighting }, + ] = await Promise.all([ + import('@codemirror/view'), + import('@codemirror/commands'), + import('@codemirror/language'), + ]); + + const languageExtension = await this._loadLanguageTask.perform(language); + + const handleUpdateExtension = EditorView.updateListener.of( + (update: ViewUpdate) => { + // toggle a class if the update has/does not have a selection + if (update.selectionSet) { + update.view.dom.classList.toggle( + 'cm-hasSelection', + !update.state.selection.main.empty + ); + } + + // call the onInput callback if the document has changed + if (!update.docChanged || this.onInput === undefined) { + return; + } + this.onInput(update.state.doc.toString()); + } + ); + + let extensions = [ + bracketMatching(), + highlightActiveLine(), + highlightActiveLineGutter(), + highlightSpecialChars(), + history(), + lineNumbers(), + keymap.of([...defaultKeymap, ...historyKeymap]), + // custom extensions + handleUpdateExtension, + // hds dark theme + hdsDarkTheme, + syntaxHighlighting(hdsDarkHighlightStyle), + ]; + + if (languageExtension !== undefined) { + extensions = [languageExtension, ...extensions]; + } + + return extensions; + }); + + private _createEditorTask = task( + { drop: true }, + async ( + element: HTMLElement, + { + language, + value, + }: Pick + ) => { + try { + const { EditorState } = await import('@codemirror/state'); + const { EditorView } = await import('@codemirror/view'); + + const extensions = await this._buildExtensionsTask.perform({ + language, + }); + + const state = EditorState.create({ + doc: value, + extensions, + }); + + const editor = new EditorView({ + state, + parent: element, + }); + + return editor; + } catch (error) { + console.error( + `\`hds-code-editor\` modifier - Failed to setup the CodeMirror editor. Error: ${JSON.stringify(error)}` + ); + } + } + ); + + private _setupTask = task( + { drop: true }, + async ( + element: HTMLElement, + _positional: PositionalArgs, + named: NamedArgs + ) => { + const { + onBlur, + onInput, + onSetup, + ariaLabel, + ariaLabelledBy, + language, + value, + } = named; + + this.onInput = onInput; + this.onBlur = onBlur; + + this.element = element; + + const editor = await this._createEditorTask.perform(element, { + language, + value, + }); + + if (editor === undefined) { + return; + } + + this.editor = editor; + + if (onBlur !== undefined) { + this._setupEditorBlurHandler(element, onBlur); + } + + this._setupEditorAriaAttributes(editor, { ariaLabel, ariaLabelledBy }); + + onSetup?.(this.editor); + } + ); +} diff --git a/packages/components/src/modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.ts b/packages/components/src/modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.ts new file mode 100644 index 00000000000..52e292b3bbf --- /dev/null +++ b/packages/components/src/modifiers/hds-code-editor/highlight-styles/hds-dark-highlight-style.ts @@ -0,0 +1,49 @@ +import { tags } from '@lezer/highlight'; +import { HighlightStyle } from '@codemirror/language'; +import { + HDS_CODE_BLOCK_BLUE, + HDS_CODE_BLOCK_GREEN, + HDS_CODE_BLOCK_ORANGE, + HDS_CODE_BLOCK_PURPLE, + HDS_CODE_BLOCK_RED, + HDS_CODE_BLOCK_CYAN, + HDS_CODE_BLOCK_WHITE, + HDS_CODE_EDITOR_COLOR_FOREGROUND_PRIMARY, +} from '../palettes/hds-dark-palette.ts'; + +const hdsDarkHighlightStyle = HighlightStyle.define([ + // Cyan | Property, url, or operator + { tag: tags.propertyName, color: HDS_CODE_BLOCK_CYAN }, + { tag: tags.url, color: HDS_CODE_BLOCK_CYAN }, + { tag: tags.operator, color: HDS_CODE_BLOCK_CYAN }, + { tag: tags.attributeValue, color: HDS_CODE_BLOCK_CYAN }, + + // Blue | Function, builtins + { tag: tags.attributeName, color: HDS_CODE_BLOCK_BLUE }, + { tag: tags.function(tags.variableName), color: HDS_CODE_BLOCK_BLUE }, + { tag: tags.function(tags.propertyName), color: HDS_CODE_BLOCK_BLUE }, + + // Orange | Strings, characters + { tag: tags.string, color: HDS_CODE_BLOCK_ORANGE }, + { tag: tags.regexp, color: HDS_CODE_BLOCK_ORANGE }, + + // Purple | Booleans, numbers + { tag: tags.bool, color: HDS_CODE_BLOCK_PURPLE }, + { tag: tags.number, color: HDS_CODE_BLOCK_PURPLE }, + + // Green | Keywords, class names, saving the world + { tag: tags.keyword, color: HDS_CODE_BLOCK_GREEN }, + { tag: tags.className, color: HDS_CODE_BLOCK_GREEN }, + + // Red | Important items + { tag: tags.deleted, color: HDS_CODE_BLOCK_RED }, + + // White | Default color within the code block, also used for punctuation + { tag: tags.name, color: HDS_CODE_BLOCK_WHITE }, + { tag: tags.punctuation, color: HDS_CODE_BLOCK_WHITE }, + + // Gray | Used for comments across languages + { tag: tags.comment, color: HDS_CODE_EDITOR_COLOR_FOREGROUND_PRIMARY }, +]); + +export default hdsDarkHighlightStyle; diff --git a/packages/components/src/modifiers/hds-code-editor/palettes/hds-dark-palette.ts b/packages/components/src/modifiers/hds-code-editor/palettes/hds-dark-palette.ts new file mode 100644 index 00000000000..44f47c6f868 --- /dev/null +++ b/packages/components/src/modifiers/hds-code-editor/palettes/hds-dark-palette.ts @@ -0,0 +1,18 @@ +export const HDS_CODE_BLOCK_WHITE = '#efeff1'; +export const HDS_CODE_BLOCK_BLUE = '#2d8eff'; +export const HDS_CODE_BLOCK_GREEN = '#86ff13'; +export const HDS_CODE_BLOCK_ORANGE = '#ffa800'; +export const HDS_CODE_BLOCK_PURPLE = '#c76cff'; +export const HDS_CODE_BLOCK_RED = '#ff3b20'; +export const HDS_CODE_BLOCK_CYAN = '#32fff7'; + +export const HDS_CODE_BLOCK_LINE_HIGHLIGHT = 'rgba(0, 74, 222, 0.2)'; +export const HDS_CODE_BLOCK_LINE_HIGHLIGHT_BORDER = '#1b5fe5'; + +export const HDS_CODE_EDITOR_COLOR_BORDER_STRONG = 'rgba(178, 182, 189, 40%)'; +export const HDS_CODE_EDITOR_COLOR_BORDER_PRIMARY = 'rgba(178, 182, 189, 20%)'; +export const HDS_CODE_EDITOR_COLOR_FOREGROUND_PRIMARY = '#d5d7db'; +export const HDS_CODE_EDITOR_COLOR_FOREGROUND_FAINT = '#878a8f'; +export const HDS_CODE_EDITOR_COLOR_FOREGROUND_HIGH_CONTRAST = '#ffffff'; +export const HDS_CODE_EDITOR_COLOR_SURFACE_PRIMARY = '#0D0E12'; +export const HDS_CODE_EDITOR_COLOR_SURFACE_FAINT = '#15181e'; diff --git a/packages/components/src/modifiers/hds-code-editor/themes/hds-dark-theme.ts b/packages/components/src/modifiers/hds-code-editor/themes/hds-dark-theme.ts new file mode 100644 index 00000000000..fbbcd6b0296 --- /dev/null +++ b/packages/components/src/modifiers/hds-code-editor/themes/hds-dark-theme.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { EditorView } from '@codemirror/view'; +import { + HDS_CODE_BLOCK_LINE_HIGHLIGHT, + HDS_CODE_BLOCK_LINE_HIGHLIGHT_BORDER, + HDS_CODE_BLOCK_GREEN, + HDS_CODE_BLOCK_WHITE, + HDS_CODE_EDITOR_COLOR_BORDER_PRIMARY, + HDS_CODE_EDITOR_COLOR_SURFACE_PRIMARY, + HDS_CODE_EDITOR_COLOR_FOREGROUND_FAINT, + HDS_CODE_EDITOR_COLOR_FOREGROUND_HIGH_CONTRAST, +} from '../palettes/hds-dark-palette.ts'; + +const hdsDark = EditorView.theme( + { + '&': { + color: HDS_CODE_BLOCK_WHITE, + backgroundColor: HDS_CODE_EDITOR_COLOR_SURFACE_PRIMARY, + height: '100%', + }, + '& ::selection': { + backgroundColor: HDS_CODE_BLOCK_GREEN, + color: HDS_CODE_EDITOR_COLOR_SURFACE_PRIMARY, + }, + '.cm-content': { + padding: '16px 0', + }, + '.cm-gutters': { + backgroundColor: HDS_CODE_EDITOR_COLOR_SURFACE_PRIMARY, + }, + '.cm-gutter': { + borderRight: `1px solid ${HDS_CODE_EDITOR_COLOR_BORDER_PRIMARY}`, + }, + '.cm-lineNumbers': { + color: HDS_CODE_EDITOR_COLOR_FOREGROUND_FAINT, + }, + '.cm-lineNumbers .cm-gutterElement': { + borderLeft: '4px solid transparent', + padding: '0 16px', + }, + '.cm-gutterElement.cm-activeLineGutter': { + backgroundColor: HDS_CODE_EDITOR_COLOR_SURFACE_PRIMARY, + }, + '&:not(.cm-hasSelection).cm-focused .cm-gutterElement.cm-activeLineGutter': + { + borderColor: HDS_CODE_BLOCK_LINE_HIGHLIGHT_BORDER, + backgroundColor: HDS_CODE_BLOCK_LINE_HIGHLIGHT, + color: HDS_CODE_EDITOR_COLOR_FOREGROUND_HIGH_CONTRAST, + outline: `1px solid ${HDS_CODE_BLOCK_LINE_HIGHLIGHT_BORDER}`, + }, + '.cm-line': { + padding: '0 16px', + }, + '.cm-activeLine': { + backgroundColor: HDS_CODE_EDITOR_COLOR_SURFACE_PRIMARY, + }, + '&:not(.cm-hasSelection).cm-focused .cm-activeLine': { + backgroundColor: HDS_CODE_BLOCK_LINE_HIGHLIGHT, + outline: `1px solid ${HDS_CODE_BLOCK_LINE_HIGHLIGHT_BORDER}`, + }, + '.cm-matchingBracket': { + outline: `1px solid ${HDS_CODE_BLOCK_WHITE}`, + }, + }, + { dark: true } +); + +export default hdsDark; diff --git a/packages/components/src/modifiers/hds-code-editor/types.ts b/packages/components/src/modifiers/hds-code-editor/types.ts new file mode 100644 index 00000000000..6f7b08fdd7a --- /dev/null +++ b/packages/components/src/modifiers/hds-code-editor/types.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +export enum HdsCodeEditorLanguageValues { + Ruby = 'ruby', + Shell = 'shell', + Go = 'go', + Hcl = 'hcl', + Json = 'json', + Sql = 'sql', + Yaml = 'yaml', +} + +export type HdsCodeEditorLanguages = `${HdsCodeEditorLanguageValues}`; diff --git a/packages/components/src/styles/@hashicorp/design-system-components.scss b/packages/components/src/styles/@hashicorp/design-system-components.scss index 95993baa817..57edcab6516 100644 --- a/packages/components/src/styles/@hashicorp/design-system-components.scss +++ b/packages/components/src/styles/@hashicorp/design-system-components.scss @@ -26,6 +26,7 @@ @use "../components/button-set"; @use "../components/card"; @use "../components/code-block"; +@use "../components/code-editor"; @use "../components/copy"; @use "../components/dialog-primitive"; @use "../components/disclosure-primitive"; diff --git a/packages/components/src/styles/components/code-editor/index.scss b/packages/components/src/styles/components/code-editor/index.scss new file mode 100644 index 00000000000..681755bf920 --- /dev/null +++ b/packages/components/src/styles/components/code-editor/index.scss @@ -0,0 +1,113 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// +// CODE-EDITOR +// + +@use "theme"; + +.hds-code-editor { + display: flex; + flex-direction: column; + overflow: hidden; + background-color: var(--hds-code-editor-color-surface-primary); + border: 1px solid var(--hds-code-editor-color-border-strong); + + &.hds-code-editor--is-full-screen { + position: fixed; + inset: 0; + z-index: 1000; + border: none; + border-radius: 0; + + .hds-code-editor__editor { + max-height: unset; + } + } + + &.hds-code-editor--is-standalone { + border-radius: var(--token-border-radius-medium); + } + + .hds-code-editor__header { + display: flex; + gap: 12px; + align-items: start; + padding: 16px; + background-color: var(--hds-code-editor-color-surface-faint); + border-bottom: 1px solid var(--hds-code-editor-color-border-primary); + } + + .hds-code-editor__header-content { + display: flex; + flex-direction: column; + flex-grow: 1; + } + + .hds-code-editor__title { + color: var(--hds-code-editor-color-foreground-primary); + + + .hds-code-editor__description { + margin-top: 4px; + } + } + + .hds-code-editor__description { + color: var(--hds-code-editor-color-foreground-faint); + } + + .hds-code-editor__title + .hds-code-editor__header-generic, + .hds-code-editor__description + .hds-code-editor__header-generic { + margin-top: 12px; + } + + .hds-code-editor__header-actions { + display: flex; + gap: 8px; + align-items: center; + + .hds-button { + outline-offset: 0; + } + } + + .hds-code-editor__editor { + flex-grow: 1; + overflow: auto; + } + + .hds-code-editor__loader { + display: flex; + align-items: center; + justify-content: center; + height: 164px; + color: var(--hds-code-editor-color-foreground-primary); + background-color: var(--hds-code-editor-color-surface-primary); + } + + .hds-button { + color: var(--hds-code-editor-color-foreground-primary); + background-color: var(--hds-code-editor-color-surface-faint); + border: 1px solid var(--hds-code-editor-color-border-strong); + + &:active { + background-color: var(--hds-code-editor-color-surface-interactive-active) + } + + &:focus, + &:hover { + background-color: var(--hds-code-editor-color-surface-primary); + + .hds-button__icon { + color: var(--hds-code-editor-color-foreground-primary); + } + } + + .hds-button__icon { + color: var(--hds-code-editor-color-foreground-primary); + } + } +} \ No newline at end of file diff --git a/packages/components/src/styles/components/code-editor/theme.scss b/packages/components/src/styles/components/code-editor/theme.scss new file mode 100644 index 00000000000..14d7d245c36 --- /dev/null +++ b/packages/components/src/styles/components/code-editor/theme.scss @@ -0,0 +1,20 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// THEMING + +.hds-code-editor--theme-dark { + // COLORS + + // Base UI colors: + --hds-code-editor-color-border-strong: rgba(178, 182, 189, 40%); + --hds-code-editor-color-border-primary: rgba(178, 182, 189, 20%); + --hds-code-editor-color-foreground-primary: #d5d7db; + --hds-code-editor-color-foreground-faint: #878a8f; + --hds-code-editor-color-foreground-high-contrast: #fff; + --hds-code-editor-color-surface-faint: #15181e; + --hds-code-editor-color-surface-primary: #0D0E12; + --hds-code-editor-color-surface-interactive-active: #2B303C; +} \ No newline at end of file diff --git a/packages/components/src/template-registry.ts b/packages/components/src/template-registry.ts index 5deaed8ae63..58839b7c541 100644 --- a/packages/components/src/template-registry.ts +++ b/packages/components/src/template-registry.ts @@ -49,6 +49,11 @@ import type HdsApplicationStateFooterComponent from './components/hds/applicatio import type HdsApplicationStateHeaderComponent from './components/hds/application-state/header'; import type HdsApplicationStateMediaComponent from './components/hds/application-state/media'; import type HdsCardContainerComponent from './components/hds/card/container.ts'; +import type HdsCodeEditorComponent from './components/hds/code-editor/index.ts'; +import type HdsCodeEditorDescriptionComponent from './components/hds/code-editor/description.ts'; +import type HdsCodeEditorGenericComponent from './components/hds/code-editor/generic.ts'; +import type HdsCodeEditorTitleComponent from './components/hds/code-editor/title.ts'; +import type HdsCodeEditorFullScreenButtonComponent from './components/hds/code-editor/full-screen-button.ts'; import type HdsCodeBlockComponent from './components/hds/code-block'; import type HdsCodeBlockCopyButtonComponent from './components/hds/code-block/copy-button'; import type HdsCodeBlockDescriptionComponent from './components/hds/code-block/description'; @@ -200,6 +205,7 @@ import type HdsFormatRelativeHelper from './helpers/hds-format-relative.ts'; // modifiers import type HdsAnchoredPositionModifier from './modifiers/hds-anchored-position.ts'; +import type HdsCodeEditorModifier from './modifiers/hds-code-editor.ts'; import type HdsClipboardModifier from './modifiers/hds-clipboard.ts'; import type HdsRegisterEventModifier from './modifiers/hds-register-event.ts'; import type HdsTooltipModifier from './modifiers/hds-tooltip.ts'; @@ -371,6 +377,18 @@ export default interface HdsComponentsRegistry { 'Hds::CodeBlock::Title': typeof HdsCodeBlockTitleComponent; 'hds/code-block/title': typeof HdsCodeBlockTitleComponent; + // Code Editor + 'Hds::CodeEditor': typeof HdsCodeEditorComponent; + 'hds/code-editor': typeof HdsCodeEditorComponent; + 'Hds::CodeEditor::Description': typeof HdsCodeEditorDescriptionComponent; + 'hds/code-editor/description': typeof HdsCodeEditorDescriptionComponent; + 'Hds::CodeEditor::Generic': typeof HdsCodeEditorGenericComponent; + 'hds/code-editor/generic': typeof HdsCodeEditorGenericComponent; + 'Hds::CodeEditor::Title': typeof HdsCodeEditorTitleComponent; + 'hds/code-editor/title': typeof HdsCodeEditorTitleComponent; + 'Hds::CodeEditor::FullScreenButton': typeof HdsCodeEditorFullScreenButtonComponent; + 'hds/code-editor/full-screen-button': typeof HdsCodeEditorFullScreenButtonComponent; + // Copy Button 'Hds::Copy::Button': typeof HdsCopyButtonComponent; 'hds/copy/button': typeof HdsCopyButtonComponent; @@ -884,6 +902,9 @@ export default interface HdsComponentsRegistry { // hds-anchored-position 'hds-anchored-position': typeof HdsAnchoredPositionModifier; + // hds-register-event + 'hds-code-editor': typeof HdsCodeEditorModifier; + // hds-clipboard 'hds-clipboard': typeof HdsClipboardModifier; diff --git a/showcase/app/controllers/components/code-editor.js b/showcase/app/controllers/components/code-editor.js new file mode 100644 index 00000000000..d07dc4fd053 --- /dev/null +++ b/showcase/app/controllers/components/code-editor.js @@ -0,0 +1,94 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Controller from '@ember/controller'; + +export default class CodeEditorController extends Controller { + demoCode = `lorem ipsum dolor sit amet +consectetur adipiscing elit +sed do eiusmod tempor incididunt +ut labore et dolore magna aliqua`; + + languages = [ + { + value: 'ruby', + label: 'Ruby', + code: `require 'date' + +file_name = 'example_file.txt' +log_file = 'script.log' + +if File.exist?(file_name) + File.open(log_file, 'a') { |f| f.puts("#{Time.now}: File #{file_name} already exists") } +else + File.open(file_name, 'w') { |f| f.puts("This is a new file.") } + File.open(log_file, 'a') { |f| f.puts("#{Time.now}: Created file #{file_name}") } +end`, + }, + { + value: 'shell', + label: 'Shell', + code: `DIR="example_directory" +LOG_FILE="script.log" + +if [ ! -d "$DIR" ]; then + mkdir "$DIR" + echo "$(date): Created directory $DIR" >> "$LOG_FILE" +else + echo "$(date): Directory $DIR already exists" >> "$LOG_FILE" +fi`, + }, + { + value: 'go', + label: 'Go', + code: `package main + +import "fmt" + +func main() { + fmt.Println("Hello, world!") + fmt.Println("Welcome to Go!") +}`, + }, + { + value: 'hcl', + label: 'HCL', + code: `variable "region" { + type = string + default = "us-west-1" +}`, + }, + { + value: 'json', + label: 'JSON', + code: `{ + "message": "Hello, world!", + "status": "success", + "data": null +}`, + }, + { + value: 'sql', + label: 'SQL', + code: `SELECT 'Hello, world!'; +SELECT 'Welcome to SQL!'; +SELECT 'Enjoy coding!';`, + }, + { + value: 'yaml', + label: 'YAML', + code: `app_config: + name: ExampleApp + version: 1.0.0 + environment: production + database: + host: localhost + port: 5432 + username: admin + password: secret + name: example_db`, + }, + ]; +} diff --git a/showcase/app/router.ts b/showcase/app/router.ts index ac108e42ebc..6a16b695a32 100644 --- a/showcase/app/router.ts +++ b/showcase/app/router.ts @@ -31,6 +31,7 @@ Router.map(function () { this.route('button-set'); this.route('card'); this.route('code-block'); + this.route('code-editor'); this.route('dropdown'); this.route('flyout'); this.route('form', function () { diff --git a/showcase/app/routes/components/code-editor.js b/showcase/app/routes/components/code-editor.js new file mode 100644 index 00000000000..6b2ccfa5d97 --- /dev/null +++ b/showcase/app/routes/components/code-editor.js @@ -0,0 +1,8 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import Route from '@ember/routing/route'; + +export default class ComponentsCodeEditorRoute extends Route {} diff --git a/showcase/app/styles/app.scss b/showcase/app/styles/app.scss index 26de1e59a90..1a460b920bd 100644 --- a/showcase/app/styles/app.scss +++ b/showcase/app/styles/app.scss @@ -40,6 +40,7 @@ @use "./showcase-pages/button" as showcase-button; @use "./showcase-pages/card" as showcase-card; @use "./showcase-pages/code-block" as showcase-code-block; +@use "./showcase-pages/code-editor" as showcase-code-editor; @use "./showcase-pages/copy/button" as showcase-copy-button; @use "./showcase-pages/copy/snippet" as showcase-copy-snippet; @use "./showcase-pages/dialog-primitive" as showcase-dialog-primitive; diff --git a/showcase/app/styles/showcase-pages/code-editor.scss b/showcase/app/styles/showcase-pages/code-editor.scss new file mode 100644 index 00000000000..b78534bedcb --- /dev/null +++ b/showcase/app/styles/showcase-pages/code-editor.scss @@ -0,0 +1,12 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// CODE-EDITOR + +.my-code-editor-custom-content { + display: flex; + gap: 8px; + align-items: center; +} diff --git a/showcase/app/templates/components/code-editor.hbs b/showcase/app/templates/components/code-editor.hbs new file mode 100644 index 00000000000..3a28a2ed67a --- /dev/null +++ b/showcase/app/templates/components/code-editor.hbs @@ -0,0 +1,137 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + +{{page-title "CodeEditor Component"}} + +CodeEditor + +
+ Content + + + + + + + + + + Standalone + + + + + + + + + + Header + + Title and description + + + + Code editor with title + + + + + This is a code editor with a description + + + + + Code editor with title + This is a code editor with a description + + + + + Custom content + + + + + + + + + + + Code editor with custom content and title + + + + + + + + Description for code editor with custom content and description + + + + + + + + Code editor with custom content, title, and description + Description for code editor with custom content, title, and description + + + + + + + + Actions + + + + + + + + + + + + + Complex example + + + + Code editor with title + This is a code editor with a description + + + + + + + + + Syntax highlighting + + {{#each this.languages as |lang|}} + + + {{lang.label}} + + + {{/each}} + + + Standalone modifier usage + + +
+ + +
\ No newline at end of file diff --git a/showcase/app/templates/index.hbs b/showcase/app/templates/index.hbs index 763ca52e3d8..b5795a732a1 100644 --- a/showcase/app/templates/index.hbs +++ b/showcase/app/templates/index.hbs @@ -93,6 +93,11 @@ CodeBlock +
  • + + CodeEditor + +
  • Copy::Button diff --git a/showcase/tests/acceptance/components/hds/code-editor.js b/showcase/tests/acceptance/components/hds/code-editor.js new file mode 100644 index 00000000000..b9fb92e2b55 --- /dev/null +++ b/showcase/tests/acceptance/components/hds/code-editor.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { visit } from '@ember/test-helpers'; +import { setupApplicationTest } from 'dummy/tests/helpers'; +import { a11yAudit } from 'ember-a11y-testing/test-support'; + +module('Acceptance | components/code-editor', function (hooks) { + setupApplicationTest(hooks); + + test('Components/code-editor page passes automated a11y checks', async function (assert) { + await visit('/components/code-editor'); + + await a11yAudit(); + + assert.ok(true, 'a11y automation audit passed'); + }); +}); diff --git a/showcase/tests/acceptance/percy-test.js b/showcase/tests/acceptance/percy-test.js index aa16c0f9501..1d181970b91 100644 --- a/showcase/tests/acceptance/percy-test.js +++ b/showcase/tests/acceptance/percy-test.js @@ -4,7 +4,7 @@ */ import { module, test } from 'qunit'; -import { visit, click } from '@ember/test-helpers'; +import { visit, click, waitFor } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import percySnapshot from '@percy/ember'; import config from 'showcase/config/environment'; @@ -64,6 +64,10 @@ module('Acceptance | Percy test', function (hooks) { await visit('/components/code-block'); await percySnapshot('CodeBlock'); + await visit('/components/code-editor'); + await waitFor('.hds-code-editor__loader', { count: 0 }); + await percySnapshot('CodeEditor'); + await visit('/components/copy/button'); await percySnapshot('CopyButton'); diff --git a/showcase/tests/integration/components/hds/code-editor/full-screen-button-test.js b/showcase/tests/integration/components/hds/code-editor/full-screen-button-test.js new file mode 100644 index 00000000000..671f0a43f1c --- /dev/null +++ b/showcase/tests/integration/components/hds/code-editor/full-screen-button-test.js @@ -0,0 +1,76 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { click, render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import sinon from 'sinon'; + +module( + 'Integration | Component | hds/code-editor/full-screen-button', + function (hooks) { + setupRenderingTest(hooks); + + test('it should render the component with a CSS class that matches the component name', async function (assert) { + this.set('noop', () => {}); + + await render( + hbs`` + ); + + assert.dom('.hds-code-editor__full-screen-button').exists(); + }); + + // @isFullScreen + test('it should render the component with the correct class and icon based on the `@isFullScreen` argument', async function (assert) { + this.setProperties({ + noop: () => {}, + isFullScreen: false, + }); + + await render( + hbs`` + ); + assert + .dom('.hds-code-editor__full-screen-button') + .doesNotHaveClass('hds-code-editor__full-screen-button--minimize') + .hasClass('hds-code-editor__full-screen-button--maximize'); + assert + .dom('.hds-code-editor__full-screen-button .hds-icon-maximize') + .exists(); + assert + .dom('.hds-code-editor__full-screen-button .hds-icon-minimize') + .doesNotExist(); + + this.set('isFullScreen', true); + assert + .dom('.hds-code-editor__full-screen-button') + .doesNotHaveClass('hds-code-editor__full-screen-button--maximize') + .hasClass('hds-code-editor__full-screen-button--minimize'); + assert + .dom('.hds-code-editor__full-screen-button .hds-icon-maximize') + .doesNotExist(); + assert + .dom('.hds-code-editor__full-screen-button .hds-icon-minimize') + .exists(); + }); + + // @onToggleFullScreen + test('it should call the `@onToggleFullScreen` action when the button is clicked', async function (assert) { + const onToggleFullScreen = sinon.spy(); + + this.set('onToggleFullScreen', onToggleFullScreen); + + await render( + hbs`` + ); + + await click('#test-button'); + + assert.ok(onToggleFullScreen.calledOnce); + }); + } +); diff --git a/showcase/tests/integration/components/hds/code-editor/index-test.js b/showcase/tests/integration/components/hds/code-editor/index-test.js new file mode 100644 index 00000000000..4eaf002d804 --- /dev/null +++ b/showcase/tests/integration/components/hds/code-editor/index-test.js @@ -0,0 +1,257 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { click, render, waitFor } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import sinon from 'sinon'; + +async function setupCodeEditor(hbsTemplate) { + await render(hbsTemplate); + return waitFor('.cm-editor'); +} + +module('Integration | Component | hds/code-editor/index', function (hooks) { + setupRenderingTest(hooks); + + test('it should render the component with a CSS class that matches the component name', async function (assert) { + await setupCodeEditor( + hbs`` + ); + assert.dom('#test-code-editor').hasClass('hds-code-editor'); + }); + + // title + test('it should render the component with a title using the default tag', async function (assert) { + await setupCodeEditor( + hbs`Test Title` + ); + assert + .dom('.hds-code-editor__title') + .hasTagName('h2') + .hasText('Test Title'); + }); + test('it should render the component title with a custom tag when provided', async function (assert) { + await setupCodeEditor( + hbs`Test Title` + ); + assert.dom('.hds-code-editor__title').hasTagName('h1'); + }); + test('it should not render the component with a title when the `Title` contextual component is not yielded', async function (assert) { + await setupCodeEditor(hbs``); + assert.dom('.hds-code-editor__title').doesNotExist(); + }); + test('when aria-label is not provided and the `Title` contextual component is yielded, it should use the title element id as the aria-labelledby value', async function (assert) { + await setupCodeEditor( + hbs`Test Title` + ); + assert + .dom('.hds-code-editor__editor .cm-editor [role="textbox"]') + .hasAttribute('aria-labelledby', 'test-title'); + }); + + // description + test('it should render the component with a description', async function (assert) { + await setupCodeEditor( + hbs`Test Description` + ); + assert.dom('.hds-code-editor__description').hasText('Test Description'); + }); + test('it should not render the component with a description when the `description` contextual component is not yielded', async function (assert) { + await setupCodeEditor(hbs``); + assert.dom('hds-code-editor__description').doesNotExist(); + }); + + // yielded block content + test('it should render custom content in the toolbar when provided', async function (assert) { + await setupCodeEditor(hbs` + + + + `); + assert.dom('#test-toolbar-button').exists(); + }); + // @hasCopyButton + test('it should render a copy button when the `@hasCopyButton` argument is true', async function (assert) { + await setupCodeEditor( + hbs`` + ); + assert.dom('.hds-code-editor__copy-button').exists(); + }); + test('it should not render a copy button when the `@hasCopyButton` argument is not provided', async function (assert) { + await setupCodeEditor(hbs``); + assert.dom('.hds-code-editor__copy-button').doesNotExist(); + }); + // @isStandalone + test('it should render the component with a standalone style when the `@isStandalone` argument is true and when the argument is ommitted', async function (assert) { + this.set('isStandalone', true); + + await setupCodeEditor( + hbs`` + ); + assert.dom('.hds-code-editor').hasClass('hds-code-editor--is-standalone'); + + this.set('isStandalone', undefined); + assert.dom('.hds-code-editor').hasClass('hds-code-editor--is-standalone'); + + this.set('isStandalone', false); + assert + .dom('.hds-code-editor') + .doesNotHaveClass('hds-code-editor--is-standalone'); + }); + + // @hasFullScreenButton + test('it should render a toggle fullscreen button when the `@hasFullScreenButton` argument is true', async function (assert) { + await setupCodeEditor( + hbs`` + ); + assert.dom('.hds-code-editor__full-screen-button').exists(); + }); + test('it should not render a toggle fullscreen button when the `@hasFullScreenButton` argument is not provided', async function (assert) { + await setupCodeEditor(hbs``); + assert.dom('.hds-code-editor__full-screen-button').doesNotExist(); + }); + + // expand/colapse + test('it should expand the code editor when the toggle full screen button is clicked', async function (assert) { + await setupCodeEditor( + hbs`` + ); + // initial state + assert + .dom('.hds-code-editor') + .doesNotHaveClass('hds-code-editor--is-full-screen'); + assert + .dom('.hds-code-editor__full-screen-button') + .doesNotHaveAttribute('aria-pressed'); + assert + .dom('.hds-code-editor__full-screen-button .hds-icon') + .hasAttribute('data-test-icon', 'maximize'); + + // expanded + await click('.hds-code-editor__full-screen-button'); + assert.dom('.hds-code-editor').hasClass('hds-code-editor--is-full-screen'); + assert + .dom('.hds-code-editor__full-screen-button') + .hasAttribute('aria-pressed'); + assert + .dom('.hds-code-editor__full-screen-button .hds-icon') + .hasAttribute('data-test-icon', 'minimize'); + + // collapsed + await click('.hds-code-editor__full-screen-button'); + assert + .dom('.hds-code-editor') + .doesNotHaveClass('hds-code-editor--is-full-screen'); + assert + .dom('.hds-code-editor__full-screen-button') + .doesNotHaveAttribute('aria-pressed'); + assert + .dom('.hds-code-editor__full-screen-button .hds-icon') + .hasAttribute('data-test-icon', 'maximize'); + }); + + // copy + test('it should copy the code editor value to the clipboard when the copy button is clicked', async function (assert) { + const clipboardStub = sinon.stub(window.navigator.clipboard, 'writeText'); + + this.setProperties({ + handleInput: () => {}, + handleSetup: (editorView) => { + this.set('editorView', editorView); + }, + }); + + await setupCodeEditor( + hbs`` + ); + + await click('.hds-code-editor__copy-button'); + assert.true(clipboardStub.calledWith('Test Code')); + + this.editorView.dispatch({ + changes: { + from: this.editorView.state.selection.main.from, + insert: 'Additional text. ', + }, + }); + + await click('.hds-code-editor__copy-button'); + assert.true(clipboardStub.calledWith('Additional text. Test Code')); + + sinon.restore(); + }); + + // @ariaLabel + test('it should render the component with an aria-label when provided', async function (assert) { + await setupCodeEditor( + hbs`` + ); + assert + .dom('.hds-code-editor__editor .cm-editor [role="textbox"]') + .hasAttribute('aria-label', 'Test Code Editor'); + }); + + // @ariaLabelledBy + test('it should render the component with an aria-labelledby when provided', async function (assert) { + await setupCodeEditor( + hbs`` + ); + assert + .dom('.hds-code-editor__editor .cm-editor [role="textbox"]') + .hasAttribute('aria-labelledby', 'test-label'); + }); + test('it should not render the component with an aria-labbelledby when @ariaLabel is provided as well', async function (assert) { + await setupCodeEditor( + hbs`` + ); + assert + .dom('.hds-code-editor__editor .cm-editor [role="textbox"]') + .hasAttribute('aria-label', 'Test Code Editor'); + assert + .dom('.hds-code-editor__editor .cm-editor [role="textbox"]') + .doesNotHaveAttribute('aria-labelledby'); + }); + + // @value + test('it should render the component with the provided value', async function (assert) { + await setupCodeEditor( + hbs`` + ); + assert.dom('.hds-code-editor__editor .cm-editor').includesText('Test Code'); + }); + + // @onInput + test('it should call the onInput action when the code editor value changes', async function (assert) { + const inputSpy = sinon.spy(); + + this.setProperties({ + handleInput: inputSpy, + handleSetup: (editorView) => { + this.set('editorView', editorView); + }, + }); + + await setupCodeEditor( + hbs`` + ); + + this.editorView.dispatch({ + changes: { + from: this.editorView.state.selection.main.from, + insert: 'Test string', + }, + }); + + assert.ok(inputSpy.calledOnceWith('Test string')); + }); +}); diff --git a/showcase/tests/integration/components/hds/code-editor/title-test.js b/showcase/tests/integration/components/hds/code-editor/title-test.js new file mode 100644 index 00000000000..1eecb78f3e4 --- /dev/null +++ b/showcase/tests/integration/components/hds/code-editor/title-test.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import sinon from 'sinon'; + +module('Integration | Component | hds/code-editor/title', function (hooks) { + setupRenderingTest(hooks); + + test('it should render the component with a CSS class that matches the component name', async function (assert) { + this.set('noop', () => {}); + + await render( + hbs`` + ); + + assert.dom('.hds-code-editor__title').exists(); + }); + + test('it should render the component with a title using the default tag', async function (assert) { + this.set('noop', () => {}); + + await render( + hbs`Test Title` + ); + + assert + .dom('.hds-code-editor__title') + .hasTagName('h2') + .hasText('Test Title'); + }); + + // @tag + test('it shoud render the component title with a custom tag when provided', async function (assert) { + this.set('noop', () => {}); + + await render( + hbs`Test Title` + ); + + assert.dom('.hds-code-editor__title').hasTagName('h1'); + }); + + // @onInsert + test('it should call the `@onInsert` action when the title is inserted', async function (assert) { + const onInsert = sinon.spy(); + this.set('onInsert', onInsert); + + await render( + hbs`Test Title` + ); + + assert.true(onInsert.calledOnce); + }); +}); diff --git a/showcase/tests/integration/modifiers/hds-code-editor-test.js b/showcase/tests/integration/modifiers/hds-code-editor-test.js new file mode 100644 index 00000000000..832f8e5714a --- /dev/null +++ b/showcase/tests/integration/modifiers/hds-code-editor-test.js @@ -0,0 +1,114 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { + render, + waitFor, + setupOnerror, + focus, + blur, +} from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import sinon from 'sinon'; + +async function setupCodeEditor(hbsTemplate) { + await render(hbsTemplate); + return waitFor('.cm-editor'); +} + +module('Integration | Modifier | hds-code-editor', function (hooks) { + setupRenderingTest(hooks); + + test('it converts the element it is applied to into a CodeMirror editor', async function (assert) { + await setupCodeEditor( + hbs`
    ` + ); + assert + .dom('#code-editor-wrapper .cm-editor') + .exists('code editor is rendered'); + }); + + // value + test('it should render the editor with the provided value', async function (assert) { + const val = 'Test Code'; + this.set('val', val); + await setupCodeEditor( + hbs`
    ` + ); + assert.dom('#code-editor-wrapper .cm-editor').includesText(val); + }); + + // onBlur + test('it should call the onBlur action when the code editor loses focus', async function (assert) { + const blurSpy = sinon.spy(); + + this.set('handleBlur', blurSpy); + + await setupCodeEditor( + hbs`
    ` + ); + + await focus('.cm-content'); + await blur('.cm-content'); + + assert.ok(blurSpy.calledOnce); + }); + + // onInput + test('it should call the onInput action when the code editor value changes', async function (assert) { + const inputSpy = sinon.spy(); + + this.setProperties({ + handleInput: inputSpy, + handleSetup: (editorView) => { + this.set('editorView', editorView); + }, + }); + + await setupCodeEditor( + hbs`
    ` + ); + + this.editorView.dispatch({ + changes: { + from: this.editorView.state.selection.main.from, + insert: 'Test string', + }, + }); + + assert.ok(inputSpy.calledOnceWith('Test string')); + }); + + // ariaLabel + test('it should render the editor with an aria-label when provided', async function (assert) { + await setupCodeEditor( + hbs`
    ` + ); + assert + .dom('#code-editor-wrapper .cm-editor [role="textbox"]') + .hasAttribute('aria-label', 'Test Code Editor'); + }); + + // ariaLabelledBy + test('it should render the editor with an aria-labelledby when provided', async function (assert) { + await setupCodeEditor( + hbs`
    ` + ); + assert + .dom('#code-editor-wrapper .cm-editor [role="textbox"]') + .hasAttribute('aria-labelledby', 'test-label'); + }); + + // ASSERTIONS + + test('it should throw an assertion if both ariaLabel and ariaLabelledBy are ommitted', async function (assert) { + const errorMessage = + '`hds-code-editor` modifier - Either `ariaLabel` or `ariaLabelledBy` must be provided'; + setupOnerror(function (error) { + assert.strictEqual(error.message, `Assertion Failed: ${errorMessage}`); + }); + await render(hbs`
    `); + assert.throws(function () { + throw new Error(errorMessage); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index c6a4e0e4a12..53145f11356 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1764,7 +1764,17 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.13, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.26.3, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.13, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.26.0 + resolution: "@babel/types@npm:7.26.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10/40780741ecec886ed9edae234b5eb4976968cc70d72b4e5a40d55f83ff2cc457de20f9b0f4fe9d858350e43dab0ea496e7ef62e2b2f08df699481a76df02cd6e + languageName: node + linkType: hard + +"@babel/types@npm:^7.26.3": version: 7.26.3 resolution: "@babel/types@npm:7.26.3" dependencies: @@ -2077,6 +2087,125 @@ __metadata: languageName: node linkType: hard +"@codemirror/autocomplete@npm:^6.0.0": + version: 6.18.4 + resolution: "@codemirror/autocomplete@npm:6.18.4" + dependencies: + "@codemirror/language": "npm:^6.0.0" + "@codemirror/state": "npm:^6.0.0" + "@codemirror/view": "npm:^6.17.0" + "@lezer/common": "npm:^1.0.0" + checksum: 10/7cd62db2ee87d6cb3936ced4f211bdcd9ce75515f64922119370c303a8bdbf5640e640a8d802c30d01cb55fb6c9b5299426ff799f04cff0bef8d0c3450fe2528 + languageName: node + linkType: hard + +"@codemirror/commands@npm:^6.8.0": + version: 6.8.0 + resolution: "@codemirror/commands@npm:6.8.0" + dependencies: + "@codemirror/language": "npm:^6.0.0" + "@codemirror/state": "npm:^6.4.0" + "@codemirror/view": "npm:^6.27.0" + "@lezer/common": "npm:^1.1.0" + checksum: 10/0c7991736bc84d0a7f8e49c311c78a78def84fca36ff61c2f9887067e2c06b1b232711cb99282cd410527ddbe0671d2b9aff93e0848fa731a2014c3691c737ea + languageName: node + linkType: hard + +"@codemirror/lang-go@npm:^6.0.1": + version: 6.0.1 + resolution: "@codemirror/lang-go@npm:6.0.1" + dependencies: + "@codemirror/autocomplete": "npm:^6.0.0" + "@codemirror/language": "npm:^6.6.0" + "@codemirror/state": "npm:^6.0.0" + "@lezer/common": "npm:^1.0.0" + "@lezer/go": "npm:^1.0.0" + checksum: 10/6e361bddb35683b225e1367807f598044b861c6858c9a011227fb73a872735985141746b3c410dcd8ef11b4c0e54819e720c5e663201a6a5e69ba8a9519fa287 + languageName: node + linkType: hard + +"@codemirror/lang-json@npm:^6.0.1": + version: 6.0.1 + resolution: "@codemirror/lang-json@npm:6.0.1" + dependencies: + "@codemirror/language": "npm:^6.0.0" + "@lezer/json": "npm:^1.0.0" + checksum: 10/7ce35d345bf9b2f5d96e2502a9693c8b2e74981ccf3a7a20da48e405c2bd6067b39acfd9b31fe3bbb5f9f28ccdde5ff7c52253c6d5b3be84b29df6d5db0b3b9b + languageName: node + linkType: hard + +"@codemirror/lang-sql@npm:^6.8.0": + version: 6.8.0 + resolution: "@codemirror/lang-sql@npm:6.8.0" + dependencies: + "@codemirror/autocomplete": "npm:^6.0.0" + "@codemirror/language": "npm:^6.0.0" + "@codemirror/state": "npm:^6.0.0" + "@lezer/common": "npm:^1.2.0" + "@lezer/highlight": "npm:^1.0.0" + "@lezer/lr": "npm:^1.0.0" + checksum: 10/a226e6b8dc2ada3720acb65dc615b76754fde9fd85bfbaf7dcf49115802b9e031d251fa26ab6d97dbd04f777c631a2fe0622b8fca503a9fda1e727e94e2d68e9 + languageName: node + linkType: hard + +"@codemirror/lang-yaml@npm:^6.1.2": + version: 6.1.2 + resolution: "@codemirror/lang-yaml@npm:6.1.2" + dependencies: + "@codemirror/autocomplete": "npm:^6.0.0" + "@codemirror/language": "npm:^6.0.0" + "@codemirror/state": "npm:^6.0.0" + "@lezer/common": "npm:^1.2.0" + "@lezer/highlight": "npm:^1.2.0" + "@lezer/lr": "npm:^1.0.0" + "@lezer/yaml": "npm:^1.0.0" + checksum: 10/1a1ad16554b27d9f66ad2a342170e7c7e51781876280727790e763e9a770163772d4880a9c344705ca65acc2b5fb228962dc3281ba05a71d2c071515541258ae + languageName: node + linkType: hard + +"@codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.10.3, @codemirror/language@npm:^6.6.0": + version: 6.10.8 + resolution: "@codemirror/language@npm:6.10.8" + dependencies: + "@codemirror/state": "npm:^6.0.0" + "@codemirror/view": "npm:^6.23.0" + "@lezer/common": "npm:^1.1.0" + "@lezer/highlight": "npm:^1.0.0" + "@lezer/lr": "npm:^1.0.0" + style-mod: "npm:^4.0.0" + checksum: 10/63b83b41d9f8475f757144cc204df08834bb14411c484aa265ffa3e93b7d8f696a21110df72101159a8675eda29018c6d08f864965bd4651b607a39f10ad32ed + languageName: node + linkType: hard + +"@codemirror/legacy-modes@npm:^6.4.2": + version: 6.4.2 + resolution: "@codemirror/legacy-modes@npm:6.4.2" + dependencies: + "@codemirror/language": "npm:^6.0.0" + checksum: 10/2d32742b6fb457aad8bc3de4d8019ef305000038e488ed7a564d62e1c1f5d8c45fade2bd4fdadf1b6e80401ef5694b2b48a86c5a955e06c81cfa6b33504a14fb + languageName: node + linkType: hard + +"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.5.0": + version: 6.5.0 + resolution: "@codemirror/state@npm:6.5.0" + dependencies: + "@marijn/find-cluster-break": "npm:^1.0.0" + checksum: 10/7d29461ee05851b03aadd84fed5ce55430b396097954cf47f464840a0b9af3f896375c0fc52726c50039e58bb25755e9a55ad63c6ba65646ac49e62af9cc35b6 + languageName: node + linkType: hard + +"@codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.36.2": + version: 6.36.2 + resolution: "@codemirror/view@npm:6.36.2" + dependencies: + "@codemirror/state": "npm:^6.5.0" + style-mod: "npm:^4.1.0" + w3c-keyname: "npm:^2.2.4" + checksum: 10/9ef7fcf4f9d9b6e66645ae65da1bf0c90e08f6ba786de0373b9f3644632066b91b8ea20faf67bb81eb9adf310ae76888cc7fd0901e2bb4821193f5427455c137 + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -2813,7 +2942,7 @@ __metadata: languageName: node linkType: hard -"@embroider/macros@npm:1.16.10, @embroider/macros@npm:^0.50.0 || ^1.0.0, @embroider/macros@npm:^1.0.0, @embroider/macros@npm:^1.10.0, @embroider/macros@npm:^1.13.1, @embroider/macros@npm:^1.16.5, @embroider/macros@npm:^1.16.9, @embroider/macros@npm:^1.2.0, @embroider/macros@npm:^1.8.1, @embroider/macros@npm:^1.8.3": +"@embroider/macros@npm:1.16.10": version: 1.16.10 resolution: "@embroider/macros@npm:1.16.10" dependencies: @@ -2834,6 +2963,27 @@ __metadata: languageName: node linkType: hard +"@embroider/macros@npm:^0.50.0 || ^1.0.0, @embroider/macros@npm:^1.0.0, @embroider/macros@npm:^1.10.0, @embroider/macros@npm:^1.13.1, @embroider/macros@npm:^1.16.5, @embroider/macros@npm:^1.16.9, @embroider/macros@npm:^1.2.0, @embroider/macros@npm:^1.8.1, @embroider/macros@npm:^1.8.3": + version: 1.16.9 + resolution: "@embroider/macros@npm:1.16.9" + dependencies: + "@embroider/shared-internals": "npm:2.8.1" + assert-never: "npm:^1.2.1" + babel-import-util: "npm:^2.0.0" + ember-cli-babel: "npm:^7.26.6" + find-up: "npm:^5.0.0" + lodash: "npm:^4.17.21" + resolve: "npm:^1.20.0" + semver: "npm:^7.3.2" + peerDependencies: + "@glint/template": ^1.0.0 + peerDependenciesMeta: + "@glint/template": + optional: true + checksum: 10/2adb55ba49878247bbef6fba50f2823d2445c8ceec6d32793b439c88bf098f16cb3613d78e332d97e8e163498827eecaab0f46ef3437826b5e7d9f7cf9099707 + languageName: node + linkType: hard + "@embroider/shared-internals@npm:2.8.1, @embroider/shared-internals@npm:^2.0.0, @embroider/shared-internals@npm:^2.8.1": version: 2.8.1 resolution: "@embroider/shared-internals@npm:2.8.1" @@ -3772,6 +3922,15 @@ __metadata: "@babel/plugin-transform-private-methods": "npm:^7.25.9" "@babel/plugin-transform-typescript": "npm:^7.26.3" "@babel/runtime": "npm:^7.26.0" + "@codemirror/commands": "npm:^6.8.0" + "@codemirror/lang-go": "npm:^6.0.1" + "@codemirror/lang-json": "npm:^6.0.1" + "@codemirror/lang-sql": "npm:^6.8.0" + "@codemirror/lang-yaml": "npm:^6.1.2" + "@codemirror/language": "npm:^6.10.3" + "@codemirror/legacy-modes": "npm:^6.4.2" + "@codemirror/state": "npm:^6.5.0" + "@codemirror/view": "npm:^6.36.2" "@ember/render-modifiers": "npm:^2.1.0" "@ember/string": "npm:^3.1.1" "@ember/test-helpers": "npm:^4.0.4" @@ -3799,6 +3958,7 @@ __metadata: "@typescript-eslint/parser": "npm:^8.18.1" babel-plugin-ember-template-compilation: "npm:^2.3.0" clipboard-polyfill: "npm:^4.1.1" + codemirror-lang-hcl: "npm:^0.0.0-beta.2" concurrently: "npm:^9.1.0" decorator-transforms: "npm:^1.2.1" ember-a11y-refocus: "npm:^4.1.4" @@ -4356,6 +4516,64 @@ __metadata: languageName: node linkType: hard +"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0": + version: 1.2.3 + resolution: "@lezer/common@npm:1.2.3" + checksum: 10/dad24e353e4e67d88b203191361ca1dff26c01c2b7b4ae829b668a1d115929334d077217367683e39180c0556510ed2066ea8ddba2b079be7c08a7152208cc87 + languageName: node + linkType: hard + +"@lezer/go@npm:^1.0.0": + version: 1.0.0 + resolution: "@lezer/go@npm:1.0.0" + dependencies: + "@lezer/common": "npm:^1.2.0" + "@lezer/highlight": "npm:^1.0.0" + "@lezer/lr": "npm:^1.0.0" + checksum: 10/3a7a7be931308852261e69f741e5a8edbb731aa53ba9287a103dfd66572894fd26c33c9b6f48df123352fbcf8d937a1fa482a5d7aaec37e402f0443bd99c060e + languageName: node + linkType: hard + +"@lezer/highlight@npm:^1.0.0, @lezer/highlight@npm:^1.2.0": + version: 1.2.1 + resolution: "@lezer/highlight@npm:1.2.1" + dependencies: + "@lezer/common": "npm:^1.0.0" + checksum: 10/fec3082419ee87fb265039b680fbac6796f862d8e3042dcb860e8c5a34291503a74927302b568ff1a626f0d2b5cf8dae02a51cfd200084eb329e5fd1236c3163 + languageName: node + linkType: hard + +"@lezer/json@npm:^1.0.0": + version: 1.0.3 + resolution: "@lezer/json@npm:1.0.3" + dependencies: + "@lezer/common": "npm:^1.2.0" + "@lezer/highlight": "npm:^1.0.0" + "@lezer/lr": "npm:^1.0.0" + checksum: 10/48e7b945fdfa2b5b6f862e27bc31f3991cba93f18df7fed0059b25f119b64dedd50bbc709d279e16e2b3eee10e7758d7d80c6d98d21bc15c284809d268837897 + languageName: node + linkType: hard + +"@lezer/lr@npm:^1.0.0, @lezer/lr@npm:^1.4.0": + version: 1.4.2 + resolution: "@lezer/lr@npm:1.4.2" + dependencies: + "@lezer/common": "npm:^1.0.0" + checksum: 10/f7b505906c8d8df14c07866553cf3dae1e065b1da8b28fbb4193fd67ab8d187eb45f92759e29a2cfe4283296f0aa864b38a0a91708ecfc3e24b8f662d626e0c6 + languageName: node + linkType: hard + +"@lezer/yaml@npm:^1.0.0": + version: 1.0.3 + resolution: "@lezer/yaml@npm:1.0.3" + dependencies: + "@lezer/common": "npm:^1.2.0" + "@lezer/highlight": "npm:^1.0.0" + "@lezer/lr": "npm:^1.4.0" + checksum: 10/6697b964403dc5dec9186732c5997675e5140ef5dddc8371dd28fa194d8431d8a7d5f18670be47b81a0b4ad6cbfe82e4f7c9c6f06e6f763bd100f7a38908baf5 + languageName: node + linkType: hard + "@lint-todo/utils@npm:^13.1.1": version: 13.1.1 resolution: "@lint-todo/utils@npm:13.1.1" @@ -4397,6 +4615,13 @@ __metadata: languageName: node linkType: hard +"@marijn/find-cluster-break@npm:^1.0.0": + version: 1.0.2 + resolution: "@marijn/find-cluster-break@npm:1.0.2" + checksum: 10/92fe7ba43ce3d3314f593e4c2fd822d7089649baff47a474fe04b83e3119931d7cf58388747d429ff65fa2db14f5ca57e787268c482e868fc67759511f61f09b + languageName: node + linkType: hard + "@mdn/browser-compat-data@npm:^5.5.49": version: 5.6.28 resolution: "@mdn/browser-compat-data@npm:5.6.28" @@ -5937,12 +6162,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:^22.10.2, @types/node@npm:^22.8.7": - version: 22.10.5 - resolution: "@types/node@npm:22.10.5" +"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:^22.8.7": + version: 22.10.1 + resolution: "@types/node@npm:22.10.1" dependencies: undici-types: "npm:~6.20.0" - checksum: 10/a5366961ffa9921e8f15435bc18ea9f8b7a7bb6b3d92dd5e93ebcd25e8af65708872bd8e6fee274b4655bab9ca80fbff9f0e42b5b53857790f13cf68cf4cbbfc + checksum: 10/c802a526da2f3fa3ccefd00a71244e7cb825329951719e79e8fec62b1dbc2855388c830489770611584665ce10be23c05ed585982038b24924e1ba2c2cce03fd languageName: node linkType: hard @@ -5960,6 +6185,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^22.10.2": + version: 22.10.5 + resolution: "@types/node@npm:22.10.5" + dependencies: + undici-types: "npm:~6.20.0" + checksum: 10/a5366961ffa9921e8f15435bc18ea9f8b7a7bb6b3d92dd5e93ebcd25e8af65708872bd8e6fee274b4655bab9ca80fbff9f0e42b5b53857790f13cf68cf4cbbfc + languageName: node + linkType: hard + "@types/node@npm:^9.6.0": version: 9.6.61 resolution: "@types/node@npm:9.6.61" @@ -8477,7 +8711,7 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.3, body-parser@npm:^1.19.0, body-parser@npm:^1.19.1": +"body-parser@npm:1.20.3": version: 1.20.3 resolution: "body-parser@npm:1.20.3" dependencies: @@ -8497,6 +8731,26 @@ __metadata: languageName: node linkType: hard +"body-parser@npm:^1.19.0, body-parser@npm:^1.19.1": + version: 1.20.2 + resolution: "body-parser@npm:1.20.2" + dependencies: + bytes: "npm:3.1.2" + content-type: "npm:~1.0.5" + debug: "npm:2.6.9" + depd: "npm:2.0.0" + destroy: "npm:1.2.0" + http-errors: "npm:2.0.0" + iconv-lite: "npm:0.4.24" + on-finished: "npm:2.4.1" + qs: "npm:6.11.0" + raw-body: "npm:2.5.2" + type-is: "npm:~1.6.18" + unpipe: "npm:1.0.0" + checksum: 10/3cf171b82190cf91495c262b073e425fc0d9e25cc2bf4540d43f7e7bbca27d6a9eae65ca367b6ef3993eea261159d9d2ab37ce444e8979323952e12eb3df319a + languageName: node + linkType: hard + "body@npm:^5.1.0": version: 5.1.0 resolution: "body@npm:5.1.0" @@ -9336,7 +9590,21 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.12.0, browserslist@npm:^4.21.10, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0, browserslist@npm:^4.24.2": +"browserslist@npm:^4.0.0, browserslist@npm:^4.12.0, browserslist@npm:^4.21.10, browserslist@npm:^4.23.3, browserslist@npm:^4.24.0": + version: 4.24.4 + resolution: "browserslist@npm:4.24.4" + dependencies: + caniuse-lite: "npm:^1.0.30001688" + electron-to-chromium: "npm:^1.5.73" + node-releases: "npm:^2.0.19" + update-browserslist-db: "npm:^1.1.1" + bin: + browserslist: cli.js + checksum: 10/11fda105e803d891311a21a1f962d83599319165faf471c2d70e045dff82a12128f5b50b1fcba665a2352ad66147aaa248a9d2355a80aadc3f53375eb3de2e48 + languageName: node + linkType: hard + +"browserslist@npm:^4.24.2": version: 4.24.3 resolution: "browserslist@npm:4.24.3" dependencies: @@ -9485,7 +9753,7 @@ __metadata: languageName: node linkType: hard -"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": +"call-bind@npm:^1.0.0, call-bind@npm:^1.0.8": version: 1.0.8 resolution: "call-bind@npm:1.0.8" dependencies: @@ -9497,6 +9765,19 @@ __metadata: languageName: node linkType: hard +"call-bind@npm:^1.0.2, call-bind@npm:^1.0.7": + version: 1.0.7 + resolution: "call-bind@npm:1.0.7" + dependencies: + es-define-property: "npm:^1.0.0" + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + get-intrinsic: "npm:^1.2.4" + set-function-length: "npm:^1.2.1" + checksum: 10/cd6fe658e007af80985da5185bff7b55e12ef4c2b6f41829a26ed1eef254b1f1c12e3dfd5b2b068c6ba8b86aba62390842d81752e67dcbaec4f6f76e7113b6b7 + languageName: node + linkType: hard + "call-bound@npm:^1.0.2, call-bound@npm:^1.0.3": version: 1.0.3 resolution: "call-bound@npm:1.0.3" @@ -10112,6 +10393,17 @@ __metadata: languageName: node linkType: hard +"codemirror-lang-hcl@npm:^0.0.0-beta.2": + version: 0.0.0-beta.2 + resolution: "codemirror-lang-hcl@npm:0.0.0-beta.2" + dependencies: + "@codemirror/language": "npm:^6.0.0" + "@lezer/highlight": "npm:^1.0.0" + "@lezer/lr": "npm:^1.0.0" + checksum: 10/583246182b00c60a21de247faba4771478cb36878a4e30fa7fce344e238ed079b0d12c102dc9c80ab2014bfc985b45d8ba53a1b4ca9b2f14eb6ecb8c523bb609 + languageName: node + linkType: hard + "codemod-cli@npm:^3.2.0": version: 3.2.0 resolution: "codemod-cli@npm:3.2.0" @@ -11239,15 +11531,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.7": - version: 4.4.0 - resolution: "debug@npm:4.4.0" +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.7, debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": + version: 4.3.7 + resolution: "debug@npm:4.3.7" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a languageName: node linkType: hard @@ -11260,18 +11552,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": - version: 4.3.7 - resolution: "debug@npm:4.3.7" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a - languageName: node - linkType: hard - "decamelize-keys@npm:^1.0.0": version: 1.1.1 resolution: "decamelize-keys@npm:1.1.1" @@ -17023,16 +17303,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1": - version: 2.15.1 - resolution: "is-core-module@npm:2.15.1" - dependencies: - hasown: "npm:^2.0.2" - checksum: 10/77316d5891d5743854bcef2cd2f24c5458fb69fbc9705c12ca17d54a2017a67d0693bbf1ba8c77af376c0eef6bf6d1b27a4ab08e4db4e69914c3789bdf2ceec5 - languageName: node - linkType: hard - -"is-core-module@npm:^2.16.0": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.16.0": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -17481,7 +17752,16 @@ __metadata: languageName: node linkType: hard -"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15, is-typed-array@npm:^1.1.3": +"is-typed-array@npm:^1.1.13, is-typed-array@npm:^1.1.3": + version: 1.1.13 + resolution: "is-typed-array@npm:1.1.13" + dependencies: + which-typed-array: "npm:^1.1.14" + checksum: 10/f850ba08286358b9a11aee6d93d371a45e3c59b5953549ee1c1a9a55ba5c1dd1bd9952488ae194ad8f32a9cf5e79c8fa5f0cc4d78c00720aa0bbcf238b38062d + languageName: node + linkType: hard + +"is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.15": version: 1.1.15 resolution: "is-typed-array@npm:1.1.15" dependencies: @@ -18368,12 +18648,12 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:^3.0.2": - version: 3.1.0 - resolution: "jsesc@npm:3.1.0" +"jsesc@npm:^3.0.2, jsesc@npm:~3.0.2": + version: 3.0.2 + resolution: "jsesc@npm:3.0.2" bin: jsesc: bin/jsesc - checksum: 10/20bd37a142eca5d1794f354db8f1c9aeb54d85e1f5c247b371de05d23a9751ecd7bd3a9c4fc5298ea6fa09a100dafb4190fa5c98c6610b75952c3487f3ce7967 + checksum: 10/8e5a7de6b70a8bd71f9cb0b5a7ade6a73ae6ab55e697c74cc997cede97417a3a65ed86c36f7dd6125fe49766e8386c845023d9e213916ca92c9dfdd56e2babf3 languageName: node linkType: hard @@ -18386,15 +18666,6 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:~3.0.2": - version: 3.0.2 - resolution: "jsesc@npm:3.0.2" - bin: - jsesc: bin/jsesc - checksum: 10/8e5a7de6b70a8bd71f9cb0b5a7ade6a73ae6ab55e697c74cc997cede97417a3a65ed86c36f7dd6125fe49766e8386c845023d9e213916ca92c9dfdd56e2babf3 - languageName: node - linkType: hard - "json-buffer@npm:3.0.0": version: 3.0.0 resolution: "json-buffer@npm:3.0.0" @@ -20813,7 +21084,7 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.13.3": +"object-inspect@npm:^1.13.1, object-inspect@npm:^1.13.3": version: 1.13.3 resolution: "object-inspect@npm:1.13.3" checksum: 10/14cb973d8381c69e14d7f1c8c75044eb4caf04c6dabcf40ca5c2ce42dc2073ae0bb2a9939eeca142b0c05215afaa1cd5534adb7c8879c32cba2576e045ed8368 @@ -22319,6 +22590,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:6.11.0": + version: 6.11.0 + resolution: "qs@npm:6.11.0" + dependencies: + side-channel: "npm:^1.0.4" + checksum: 10/5a3bfea3e2f359ede1bfa5d2f0dbe54001aa55e40e27dc3e60fab814362d83a9b30758db057c2011b6f53a2d4e4e5150194b5bac45372652aecb3e3c0d4b256e + languageName: node + linkType: hard + "qs@npm:6.13.0": version: 6.13.0 resolution: "qs@npm:6.13.0" @@ -23952,7 +24232,7 @@ __metadata: languageName: node linkType: hard -"set-function-length@npm:^1.2.2": +"set-function-length@npm:^1.2.1, set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" dependencies: @@ -24199,6 +24479,18 @@ __metadata: languageName: node linkType: hard +"side-channel@npm:^1.0.4": + version: 1.0.6 + resolution: "side-channel@npm:1.0.6" + dependencies: + call-bind: "npm:^1.0.7" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.4" + object-inspect: "npm:^1.13.1" + checksum: 10/eb10944f38cebad8ad643dd02657592fa41273ce15b8bfa928d3291aff2d30c20ff777cfe908f76ccc4551ace2d1245822fdc576657cce40e9066c638ca8fa4d + languageName: node + linkType: hard + "side-channel@npm:^1.0.6, side-channel@npm:^1.1.0": version: 1.1.0 resolution: "side-channel@npm:1.1.0" @@ -25071,6 +25363,13 @@ __metadata: languageName: node linkType: hard +"style-mod@npm:^4.0.0, style-mod@npm:^4.1.0": + version: 4.1.2 + resolution: "style-mod@npm:4.1.2" + checksum: 10/9da37909d6dbc3c043ab6d18da5d997073a4698c91e86058293252493eb18aca4e44e3fb18f32fcee26dcee8785f393c6c95f3c96cc722a0dd6b8de622b5b293 + languageName: node + linkType: hard + "style-search@npm:^0.1.0": version: 0.1.0 resolution: "style-search@npm:0.1.0" @@ -27010,6 +27309,13 @@ __metadata: languageName: node linkType: hard +"w3c-keyname@npm:^2.2.4": + version: 2.2.8 + resolution: "w3c-keyname@npm:2.2.8" + checksum: 10/95bafa4c04fa2f685a86ca1000069c1ec43ace1f8776c10f226a73296caeddd83f893db885c2c220ebeb6c52d424e3b54d7c0c1e963bbf204038ff1a944fbb07 + languageName: node + linkType: hard + "w3c-xmlserializer@npm:^3.0.0": version: 3.0.0 resolution: "w3c-xmlserializer@npm:3.0.0" @@ -27433,6 +27739,19 @@ __metadata: languageName: node linkType: hard +"which-typed-array@npm:^1.1.14": + version: 1.1.16 + resolution: "which-typed-array@npm:1.1.16" + dependencies: + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.7" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.2" + checksum: 10/7106e94729632cdcedc94080442872392806b3364225156952981777f46b75d2e3b13813b5d935bdb2ac8523f8758fcf3513f7e1ed44a8e10d6c4f1029c3fa7d + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.18, which-typed-array@npm:^1.1.2": version: 1.1.18 resolution: "which-typed-array@npm:1.1.18"