diff --git a/catalog/eleventy-helpers/shortcodes/playground-example.cjs b/catalog/eleventy-helpers/shortcodes/playground-example.cjs index d40df7df58..787ba765ce 100644 --- a/catalog/eleventy-helpers/shortcodes/playground-example.cjs +++ b/catalog/eleventy-helpers/shortcodes/playground-example.cjs @@ -51,7 +51,7 @@ function playgroundExample(eleventyConfig) { - Expand interactive demo. + View interactive demo inline. +

Open interactive demo in new tab.

`; }); } diff --git a/catalog/site/css/stories.css b/catalog/site/css/stories.css new file mode 100644 index 0000000000..0cc82f1f1a --- /dev/null +++ b/catalog/site/css/stories.css @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +#dragbar { + max-width: 100%; + max-height: 100%; +} + +#editor { + margin-block: 0; + height: 100%; + box-sizing: border-box; +} + +#editor-wrapper { + height: 100%; + overflow: hidden; +} + +body { + height: 100dvh; +} + +#preview { + position: relative; +} + +#preview md-circular-progress { + inset: 50%; + transform: translate(-50%, -50%); +} diff --git a/catalog/site/css/syntax-highlight.css b/catalog/site/css/syntax-highlight.css index 831441a71f..78b0050c46 100644 --- a/catalog/site/css/syntax-highlight.css +++ b/catalog/site/css/syntax-highlight.css @@ -50,6 +50,7 @@ /* Formats the code boxes themselves */ .example playground-file-editor, +playground-file-editor, pre[class*='language-'] { padding: var(--__code-block-font-size); /* Remove the extra hard coded 3px from line number padding. */ diff --git a/catalog/site/stories/stories.html b/catalog/site/stories/stories.html new file mode 100644 index 0000000000..a0ae3b59cd --- /dev/null +++ b/catalog/site/stories/stories.html @@ -0,0 +1,97 @@ +---js +{ + pagination: { + data: "collections.component", + size: 1, + alias: "component", + before: components => { + // remove any components that don't have a dirname + return components.filter(component => component.data.dirname) + } + }, + permalink: "components/{{component.data.page.fileSlug}}/stories/index.html", + fullHeightContent: "true", + collections: ["stories"], + eleventyComputed: { + dirname: ({component}) => component.data.dirname, + } +} +--- + + + + + + + Material Web - Stories {{component.data.name}} + + + + + {% inlinecss "global.css" %} + + + + {% inlinejs "inline/apply-saved-theme.js" %} + + + + + + + + + + + + + + + + + {% inlinejs "ssr-utils/dsd-polyfill.js" %} + + + + + + +
+ +
+
+ + diff --git a/catalog/src/components/drag-playground.ts b/catalog/src/components/drag-playground.ts new file mode 100644 index 0000000000..76d8bbd1fd --- /dev/null +++ b/catalog/src/components/drag-playground.ts @@ -0,0 +1,216 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + + +import { LitElement, css, html } from 'lit'; +import { customElement, state, query } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import '@material/web/icon/icon.js'; + +/** + * A playground preview + editor with a draggable handle. + */ +@customElement('drag-playground') +export class DragPlayground extends LitElement { + static styles = css` + :host { + display: block; + --_drag-bar-height: 24px; + --_drag-bar-border-width: 1px; + --_half-drag-bar-height: calc( + (var(--_drag-bar-height) / 2) + var(--_drag-bar-border-width) + ); + } + + #wrapper { + display: flex; + flex-direction: column; + } + + :host, + #wrapper, + ::slotted(*) { + height: 100%; + } + + slot { + display: block; + overflow: hidden; + } + + [name='preview'] { + height: max( + calc( + 100% - var(--editor-percentage, 0%) - var(--_half-drag-bar-height) + ), + 0px + ); + } + + [name='editor'] { + height: max( + calc(var(--editor-percentage, 0px) - var(--_half-drag-bar-height)), + 0px + ); + } + + #drag-bar { + touch-action: none; + background-color: var(--md-sys-color-surface-container); + color: var(--md-sys-color-on-surface); + border: var(--_drag-bar-border-width) solid var(--md-sys-color-outline); + border-radius: 12px; + height: var(--_drag-bar-height); + display: flex; + justify-content: center; + align-items: center; + -webkit-user-select: none; + user-select: none; + } + + #drag-bar:hover { + background-color: var(--md-sys-color-surface-container-high); + cursor: grab; + } + + #drag-bar.isDragging { + background-color: var(--md-sys-color-inverse-surface); + color: var(--md-sys-color-inverse-on-surface); + cursor: grabbing; + } + `; + + /** + * Whether or not we are in the "dragging" state. + */ + @state() private isDragging = false; + + /** + * The percentage of the editor height. + */ + @state() private editorHeightPercent = 0; + + @query('#wrapper') private wrapperEl!: HTMLElement; + + /** + * A set of pointer IDs in the case that the user is dragging with multiple + * pointers. + */ + private pointerIds = new Set(); + + render() { + return html`
+ +
+ drag_handle +
+ +
`; + } + + private onFocus() { + this.isDragging = true; + } + + private onBlur() { + this.isDragging = false; + } + + private onKeydown(event: KeyboardEvent) { + const { key } = event; + switch (key) { + case 'ArrowRight': + case 'ArrowUp': + this.editorHeightPercent = Math.min(this.editorHeightPercent + 1, 100); + break; + case 'ArrowLeft': + case 'ArrowDown': + this.editorHeightPercent = Math.max(this.editorHeightPercent - 1, 0); + break; + case 'PageUp': + this.editorHeightPercent = Math.min(this.editorHeightPercent + 10, 100); + break; + case 'PageDown': + this.editorHeightPercent = Math.max(this.editorHeightPercent - 10, 0); + break; + case 'Home': + this.editorHeightPercent = 0; + break; + case 'End': + this.editorHeightPercent = 100; + break; + default: + break; + } + } + + private onPointerdown(event: PointerEvent) { + this.isDragging = true; + + if (this.pointerIds.has(event.pointerId)) return; + + this.pointerIds.add(event.pointerId); + (event.target as HTMLElement).setPointerCapture(event.pointerId); + } + + private onPointerup(event: PointerEvent) { + this.pointerIds.delete(event.pointerId); + (event.target as HTMLElement).releasePointerCapture(event.pointerId); + + if (this.pointerIds.size === 0) { + this.isDragging = false; + } + } + + private onPointermove(event: PointerEvent) { + if (!this.isDragging) return; + + const { clientY: mouseY } = event; + const { top: wrapperTop, bottom: wrapperBottom } = + this.wrapperEl.getBoundingClientRect(); + + // The height of the wrapper + const height = wrapperBottom - wrapperTop; + + // Calculate the percentage of the editor height in which the pointer is + // located + const editorHeightPercent = 100 - ((mouseY - wrapperTop) / height) * 100; + + // Clamp the percentage between 0 and 100 + this.editorHeightPercent = Math.min(Math.max(editorHeightPercent, 0), 100); + } +} + +declare global { + interface HTMLElementTagNameMap { + 'drag-playground': DragPlayground; + } +} diff --git a/catalog/src/pages/stories.ts b/catalog/src/pages/stories.ts new file mode 100644 index 0000000000..6c08395713 --- /dev/null +++ b/catalog/src/pages/stories.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import '../components/drag-playground.js'; diff --git a/catalog/src/ssr.ts b/catalog/src/ssr.ts index 52ea560e72..998a5f7944 100644 --- a/catalog/src/ssr.ts +++ b/catalog/src/ssr.ts @@ -12,5 +12,6 @@ import './components/catalog-component-header-title.js'; import './components/nav-drawer.js'; import './components/theme-changer.js'; import './components/top-app-bar.js'; +import './components/drag-playground.js'; // 🤫 import '@material/web/labs/item/item.js'; diff --git a/catalog/stories/components/knob-ui-components.ts b/catalog/stories/components/knob-ui-components.ts index afa51ddd8e..84839b5046 100644 --- a/catalog/stories/components/knob-ui-components.ts +++ b/catalog/stories/components/knob-ui-components.ts @@ -13,11 +13,11 @@ import '@material/web/select/filled-select.js'; import '@material/web/select/select-option.js'; import '@material/web/textfield/filled-text-field.js'; -import {css, html, LitElement} from 'lit'; -import {customElement, property} from 'lit/decorators.js'; -import {StyleInfo, styleMap} from 'lit/directives/style-map.js'; +import { css, html, LitElement } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { StyleInfo, styleMap } from 'lit/directives/style-map.js'; -import {Knob, KnobUi} from '../knobs.js'; +import { Knob, KnobUi } from '../knobs.js'; /** * A boolean Knob UI. @@ -36,7 +36,8 @@ export function boolInput(): KnobUi { touch-target="none" style="margin-inline-end: 16px;" .checked=${!!knob.latestValue} - @change="${valueChanged}"> + @change="${valueChanged}" + > ${knob.name} @@ -51,7 +52,7 @@ export function boolInput(): KnobUi { */ @customElement('knob-color-selector') export class KnobColorSelector extends LitElement { - static override styles = css` + static styles = css` :host { display: inline-block; position: relative; @@ -111,7 +112,7 @@ export class KnobColorSelector extends LitElement { private internalValue = ''; - @property({type: Boolean}) hasAlpha = false; + @property({ type: Boolean }) hasAlpha = false; set value(val: string) { const oldVal = this.internalValue; @@ -119,12 +120,12 @@ export class KnobColorSelector extends LitElement { this.requestUpdate('value', oldVal); } - @property({type: String, reflect: true}) + @property({ type: String, reflect: true }) get value() { return this.internalValue; } - override render() { + render() { return html` ${this.hasAlpha ? this.renderTextInput() : this.renderColorInput()} @@ -132,7 +133,8 @@ export class KnobColorSelector extends LitElement { { this.hasAlpha = !this.hasAlpha; - }}> + }} + > ${this.hasAlpha ? 'rgba' : 'rgb'} `; @@ -143,7 +145,8 @@ export class KnobColorSelector extends LitElement { style=${styleMap(sharedTextFieldStyles)} .value=${this.value} @change=${this.propagateEvt} - @input=${this.onInput}>`; + @input=${this.onInput} + >`; } private renderColorInput() { @@ -158,7 +161,8 @@ export class KnobColorSelector extends LitElement { id="color-picker" .value=${this.value} @change=${this.propagateEvt} - @input=${this.onInput} /> + @input=${this.onInput} + /> `; } @@ -176,17 +180,17 @@ export class KnobColorSelector extends LitElement { this.dispatchEvent(newEvt); } - override click() { + click() { const input = this.renderRoot!.querySelector( - 'input,md-filled-text-field', + 'input,md-filled-text-field' ) as HTMLElement; input.click(); input.focus(); } - override focus() { + focus() { const input = this.renderRoot!.querySelector( - 'input,md-filled-text-field', + 'input,md-filled-text-field' ) as HTMLElement; input.focus(); } @@ -226,7 +230,8 @@ export function colorPicker(opts?: ColorPickerOpts): KnobUi { + @input=${valueChanged} + > ${knob.name} @@ -245,7 +250,7 @@ const sharedTextFieldStyles: StyleInfo = { '--md-filled-field-trailing-space': '8px', '--md-filled-field-top-space': '4px', '--md-filled-field-bottom-space': '4px', - 'width': '150px', + width: '150px', 'min-width': '150px', }; @@ -272,7 +277,8 @@ export function textInput(options?: TextInputOptions): KnobUi { + @input="${valueChanged}" + > ${knob.name} @@ -313,7 +319,8 @@ export function numberInput(opts?: NumberInputOpts): KnobUi { type="number" step="${config.step}" .value="${knob.latestValue ? knob.latestValue.toString() : '0'}" - @input="${valueChanged}"> + @input="${valueChanged}" + > ${knob.name} @@ -333,7 +340,7 @@ export function button(): KnobUi { const count = knob.latestValue ?? 0; onChange(count + 1); }; - const styles = styleMap({display: 'inline-block'}); + const styles = styleMap({ display: 'inline-block' }); return html` ${knob.name} @@ -344,7 +351,10 @@ export function button(): KnobUi { } interface RadioSelectorConfig { - readonly options: ReadonlyArray<{readonly value: T; readonly label: string}>; + readonly options: ReadonlyArray<{ + readonly value: T; + readonly label: string; + }>; readonly name: string; } @@ -365,7 +375,8 @@ export function radioSelector({ name="${name}" value="${value}" @change="${valueChanged}" - ?checked="${knob.latestValue === option.value}"> + ?checked="${knob.latestValue === option.value}" + > ${option.label} `; }); @@ -375,7 +386,10 @@ export function radioSelector({ } interface SelectDropdownConfig { - readonly options: ReadonlyArray<{readonly value: T; readonly label: string}>; + readonly options: ReadonlyArray<{ + readonly value: T; + readonly label: string; + }>; } /** A select dropdown Knob UI. */ @@ -391,14 +405,17 @@ export function selectDropdown({ return html``; + > +
${option.label}
+ `; }); return html`