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_more
expand_less
- 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``;
+ }
+
+ 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`