Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 74 additions & 100 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion website/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ ember-cli-update.json
*.scss

# temporary ignore
/docs/
/docs/**/*.md
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the prettier ignore more specific so the code snippets would be fixed by prettier.

/docs/**/*.js
216 changes: 216 additions & 0 deletions website/app/components/doc/code-group/index.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import { CodeBlock } from 'ember-shiki';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import { notEq, eq } from 'ember-truth-helpers';
import { guidFor } from '@ember/object/internals';

import { HdsIcon } from '@hashicorp/design-system-components/components';
import type { HdsIconSignature } from '@hashicorp/design-system-components/components/hds/icon/index';

import DynamicTemplate from 'website/components/dynamic-template';
import docClipboard from 'website/modifiers/doc-clipboard';

interface DocCodeGroupSignature {
Args: {
filename?: string;
hbsSnippet: string;
gtsSnippet: string;
hidePreview?: boolean;
};
Blocks: {
default: [];
};
Element: HTMLDivElement;
}

// Helper to undo code escaping for display
const unescapeCode = (code: string) => {
return code.replace(/\\n/g, '\n');
};

export default class DocCodeGroup extends Component<DocCodeGroupSignature> {
@tracked currentView = 'hbs';
@tracked isExpanded = false;
@tracked copyStatus = 'idle';
@tracked copyTimer: ReturnType<typeof setTimeout> | undefined;
@tracked copyIconName: HdsIconSignature['Args']['name'] = 'clipboard-copy';
@tracked expandIconName: HdsIconSignature['Args']['name'] = 'unfold-open';

componentId = guidFor(this);

get unescapedHbsSnippet() {
return unescapeCode(this.args.hbsSnippet);
}

get unescapedGtsSnippet() {
return unescapeCode(this.args.gtsSnippet);
}

get gtsSnippet() {
if (this.isExpanded) {
return this.unescapedGtsSnippet;
}

// When not expanded, show a short GTS-like version of the HBS snippet
return this.unescapedHbsSnippet.replace(/::/g, '').trim();
}

get toggleButtonLabel() {
return this.isExpanded ? 'Collapse code' : 'Expand code';
}

get language() {
if (this.currentView === 'gts' && this.isExpanded) {
return 'gts';
}

return 'hbs';
}

get currentViewSnippet() {
return this.currentView === 'hbs'
? this.unescapedHbsSnippet
: this.gtsSnippet;
}

get copyButtonLabel() {
let label;
switch (this.copyStatus) {
case 'success':
label = 'Copied';
break;
case 'error':
label = 'Error';
break;
default:
label = 'Copy';
break;
}
return label;
}

get copyButtonClassNames() {
const classNames = ['doc-code-group__copy-button'];
if (this.copyStatus === 'success') {
classNames.push('doc-code-group__copy-button--status-success');
} else if (this.copyStatus === 'error') {
classNames.push('doc-code-group__copy-button--status-error');
}
return classNames.join(' ');
}

handleGtsExpandClick = () => {
this.isExpanded = !this.isExpanded;
this.expandIconName = this.isExpanded ? 'unfold-close' : 'unfold-open';
};

handleLanguageChange = (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.checked) {
this.currentView = input.value;
}
};

onSuccess = () => {
this.copyStatus = 'success';
this.copyIconName = 'clipboard-checked';
this.resetStatusDelayed();
};

onError = () => {
this.copyStatus = 'error';
this.copyIconName = 'alert-triangle';
this.resetStatusDelayed();
};

resetStatusDelayed() {
clearTimeout(this.copyTimer);
// make it fade back to the default state
this.copyTimer = setTimeout(() => {
this.copyStatus = 'idle';
this.copyIconName = 'clipboard-copy';
}, 2000);
}

<template>
<div class="doc-code-group">
<div class="doc-code-group__action-bar">
<fieldset
class="doc-code-group__language-picker"
aria-label="Code language"
>
<label class="doc-code-group__language-picker-option">
<span>.hbs</span>
<input
type="radio"
class="sr-only"
name="language-picker-{{this.componentId}}"
value="hbs"
checked={{eq this.currentView "hbs"}}
{{on "change" this.handleLanguageChange}}
/>
</label>
<label class="doc-code-group__language-picker-option">
<span>.gts</span>
<input
type="radio"
class="sr-only"
name="language-picker-{{this.componentId}}"
value="gts"
checked={{eq this.currentView "gts"}}
{{on "change" this.handleLanguageChange}}
/>
</label>
</fieldset>
<div class="doc-code-group__secondary-actions">
<div class="doc-code-group__copy-button-container">
<button
type="button"
aria-label={{this.copyButtonLabel}}
class={{this.copyButtonClassNames}}
{{docClipboard
text=this.currentViewSnippet
onSuccess=this.onSuccess
onError=this.onError
}}
>
<HdsIcon @name={{this.copyIconName}} />
</button>
</div>
</div>
</div>
{{#if (notEq @hidePreview "true")}}
<div class="doc-code-group__preview">
<DynamicTemplate
@templateString={{this.unescapedHbsSnippet}}
@componentId={{@filename}}
/>
</div>
{{/if}}
<div class="doc-code-block__code-snippet-wrapper">
{{#if (eq this.currentView "gts")}}
<button
type="button"
class="doc-code-group__expand-button"
{{on "click" this.handleGtsExpandClick}}
aria-label={{this.toggleButtonLabel}}
aria-expanded={{this.isExpanded}}
>
<HdsIcon @name={{this.expandIconName}} />
</button>
{{/if}}
<CodeBlock
@code={{this.currentViewSnippet}}
@language={{this.language}}
@theme="github-dark"
@showCopyButton={{false}}
/>
</div>
</div>
</template>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,33 @@
* 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';

export default class DocCopyButtonComponent extends Component {
import { HdsIcon } from '@hashicorp/design-system-components/components';
import type { HdsIconSignature } from '@hashicorp/design-system-components/components/hds/icon/index';

import docClipboard from 'website/modifiers/doc-clipboard';

interface DocCopyButtonCodeSignature {
Args: {
type?: 'solid' | 'ghost';
textToCopy: string;
textToShow?: string;
encoded?: boolean;
isFullWidth?: boolean;
id?: string;
};
Blocks: {
default: [];
};
Element: HTMLButtonElement;
}

export default class DocCopyButton extends Component<DocCopyButtonCodeSignature> {
@tracked status = 'idle';
@tracked iconName = 'clipboard-copy';
@tracked timer;
@tracked iconName: HdsIconSignature['Args']['name'] = 'clipboard-copy';
@tracked timer: ReturnType<typeof setTimeout> | undefined;

get type() {
return this.args.type ?? 'solid'; // options are `solid` or `ghost`
Expand Down Expand Up @@ -58,19 +76,17 @@ export default class DocCopyButtonComponent extends Component {
return this.args.isFullWidth ?? false;
}

@action
onSuccess() {
onSuccess = () => {
this.status = 'success';
this.iconName = 'clipboard-checked';
this.resetStatusDelayed();
}
};

@action
onError() {
onError = () => {
this.status = 'error';
this.iconName = 'alert-triangle';
this.resetStatusDelayed();
}
};

resetStatusDelayed() {
clearTimeout(this.timer);
Expand All @@ -82,11 +98,38 @@ export default class DocCopyButtonComponent extends Component {
}

get classNames() {
let classes = ['doc-copy-button'];
const classes = ['doc-copy-button'];
classes.push(`doc-copy-button--type-${this.type}`);
if (this.isFullWidth) {
classes.push(`doc-copy-button--width-full`);
}
return classes.join(' ');
}

<template>
<button
class={{this.classNames}}
type="button"
{{docClipboard
text=@textToCopy
onSuccess=this.onSuccess
onError=this.onError
}}
>
{{#if this.textToShow}}
<span class="doc-copy-button__visible-value">{{this.textToShow}}</span>
{{/if}}
{{#if this.label}}
<span
id="copy-label-{{@id}}"
class="doc-copy-button__label"
>{{this.label}}</span>
{{/if}}
<HdsIcon
class="doc-copy-button__icon"
@name={{this.iconName}}
@stretched={{true}}
/>
</button>
</template>
}
20 changes: 0 additions & 20 deletions website/app/components/doc/copy-button/index.hbs

This file was deleted.

1 change: 1 addition & 0 deletions website/app/components/doc/meta-row.gts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const DocMetaRow: TemplateOnlyComponent<DocMetaRowSignature> = <template>
{{! when we pass a single value, we have two different use cases to support }}
{{#if @copyable}}
<DocCopyButton
{{! @glint-expect-error }}
@textToCopy={{@valueToCopy}}
@textToShow={{(or @valueToShow @valueToCopy)}}
@type="ghost"
Expand Down
2 changes: 1 addition & 1 deletion website/app/components/dynamic-template.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { setComponentTemplate } from '@ember/component';
import { getOwner } from '@ember/application';
import { getOwner } from '@ember/owner';
import { compileTemplate } from '@ember/template-compilation';
import { importSync } from '@embroider/macros';
import Component from '@glimmer/component';
Expand Down
Loading